null이없는 언어에 대한 최상의 설명


225

프로그래머가 널 오류 / 예외에 대해 불평 할 때마다 누군가가 널없이 우리가 무엇을하는지 묻습니다.

나는 옵션 유형의 차가움에 대한 기본 아이디어를 가지고 있지만 그것을 표현할 수있는 지식이나 언어 기술이 없습니다. 일반 프로그래머가 접근 할 수있는 방식으로 작성된 다음에 대한 훌륭한 설명은 무엇입니까 ?

  • 참조 / 포인터가 바람직하지 않은 경우 기본적으로 널 입력 가능
  • 다음과 같은 null 사례를 쉽게 확인할 수있는 전략을 포함한 옵션 유형 작동 방식
    • 패턴 매칭
    • 수도원 이해
  • 메시지를 먹는 것과 같은 대체 솔루션
  • (내가 놓친 다른 측면들)

11
기능 프로그래밍 또는 F #에 대해이 질문에 태그를 추가하면 환상적인 답변을 얻을 수 있습니다.
Stephen Swensen

옵션 유형이 ml 세계에서 왔기 때문에 기능 프로그래밍 태그를 추가했습니다. 차라리 F # (너무 구체적)으로 표시하지 않겠습니다. BTW 분류 체계가있는 사람은 대체 유형 또는 옵션 유형 태그를 추가해야합니다.
Roman A. Taycher

4
그런 특정 태그가 거의 필요하지 않습니다. 이 태그는 주로 사람들이 관련 질문을 찾을 수 있도록하기위한 것입니다 (예 : "내가 많이 알고 있고 대답 할 수있는 질문", "기능 프로그래밍"은 매우 유용합니다. 그러나 "null"또는 " option-type "은 그다지 유용하지 않습니다. 응답 할 수있는 질문을 찾는"option-type "태그를 모니터링하는 사람은 거의 없습니다.;)
jalf

널 (null)에 대한 주된 이유 중 하나는 컴퓨터가 이론을 설정하기 위해 강력하게 연계되어 있다는 점을 잊지 마십시오. 널은 모든 세트 이론에서 가장 중요한 세트 중 하나입니다. 그것 없이는 전체 알고리즘이 무너질 것입니다. 예를 들어, 병합 정렬을 수행하십시오. 여기에는 목록을 여러 번 반으로 나누는 작업이 포함됩니다. 목록이 7 개 항목이면 어떻게됩니까? 먼저 4와 3으로 나눕니다. 그런 다음 2, 2, 2, 1입니다. 그런 다음 1, 1, 1, 1, 1, 1, 1, 그리고 .... null! Null에는 실제로 보이지 않는 목적이 있습니다. 이론적 영역에는 더 존재합니다.
stevendesu

6
@steven_desu-동의하지 않습니다. '널링 가능'언어에서 빈 목록 []에 대한 참조와 널 목록 참조를 가질 수 있습니다. 이 질문은 둘 사이의 혼란과 관련이 있습니다.
stusmith

답변:


433

나는 왜 null이 바람직하지 않은지에 대한 간결한 요약은 무의미한 상태는 표현할 수 없다는 것 입니다.

문을 모델링한다고 가정 해 봅시다. 개방, 종료, 잠금 해제 및 종료 및 잠금의 세 가지 상태 중 하나 일 수 있습니다. 이제 라인을 따라 모델링 할 수 있습니다.

class Door
    private bool isShut
    private bool isLocked

세 가지 상태를이 두 부울 변수에 매핑하는 방법이 명확합니다. 그러나 이로 인해 바람직하지 않은 네 번째 상태를 사용할 수 있습니다 isShut==false && isLocked==true. 표현으로 선택한 유형이이 상태를 허용하므로 클래스가이 상태에 도달하지 않도록 (정확하게 불변을 명시 적으로 코딩하여) 정신적 인 노력을 기울여야합니다. 반대로, 대수 데이터 형식 또는 확인 된 열거 형 언어를 사용하는 경우 정의 할 수 있습니다.

type DoorState =
    | Open | ShutAndUnlocked | ShutAndLocked

그런 다음 정의 할 수 있습니다

class Door
    private DoorState state

더 이상 걱정할 필요가 없습니다. 타입 시스템은 인스턴스가 존재할 수있는 상태가 3 가지에 불과하다는 것을 보장합니다 class Door. 이것은 컴파일시 전체 클래스의 에러를 명시 적으로 배제하는 타입 시스템의 장점입니다.

문제 null는 모든 참조 유형이 일반적으로 원치 않는 공간에서이 추가 상태를 얻는다는 것입니다. string변수는 임의의 문자열이 될 수 있거나,이 미친 추가 될 수 있습니다 null내 문제 영역에 매핑되지 않는 값입니다. Triangle목적은 세 가지가 Point자신이 가지고들, X그리고 Y값을하지만, 불행히도 Point의 또는 Triangle자체 수도 내가에서 일하고 있어요 그래프 도메인에 의미가이 미친 널 (null) 값을합니다. 등

존재하지 않는 값을 모델링하려는 경우 명시 적으로 선택해야합니다. 내가 사람들을 모델링하려는 방식으로 모든 사람 Person이 a FirstName와 a LastName을 가지고 있지만 일부 사람들 만 MiddleNames를 가지고 있다면

class Person
    private string FirstName
    private Option<string> MiddleName
    private string LastName

여기서 string널이 아닌 유형으로 가정합니다. 그런 다음 NullReferenceException누군가의 이름의 길이를 계산할 때 까다로운 불변이 없으며 예기치 않은 일이 없습니다 . 타입 시스템은 코드 MiddleName의 가능성을 설명 하는 계정을 다루는 코드를 보장하는 None반면, 코드를 다루는 코드 FirstName는 값이 있다고 가정 할 수 있습니다.

예를 들어 위의 유형을 사용하여이 바보 같은 함수를 작성할 수 있습니다.

let TotalNumCharsInPersonsName(p:Person) =
    let middleLen = match p.MiddleName with
                    | None -> 0
                    | Some(s) -> s.Length
    p.FirstName.Length + middleLen + p.LastName.Length

걱정할 필요가 없습니다. 대조적으로, 문자열과 같은 유형에 대한 nullable 참조가있는 언어에서는

class Person
    private string FirstName
    private string MiddleName
    private string LastName

당신은 같은 물건을 작성 결국

let TotalNumCharsInPersonsName(p:Person) =
    p.FirstName.Length + p.MiddleName.Length + p.LastName.Length

들어오는 Person 객체에 null이 아닌 모든 것이 변하지 않으면 폭발하거나

let TotalNumCharsInPersonsName(p:Person) =
    (if p.FirstName=null then 0 else p.FirstName.Length)
    + (if p.MiddleName=null then 0 else p.MiddleName.Length)
    + (if p.LastName=null then 0 else p.LastName.Length)

아니면

let TotalNumCharsInPersonsName(p:Person) =
    p.FirstName.Length
    + (if p.MiddleName=null then 0 else p.MiddleName.Length)
    + p.LastName.Length

p첫 번째 / 마지막이 있지만 중간이 null 일 수 있다고 가정 하거나 다른 유형의 예외를 던지는 검사 또는 누가 무엇을 알고 있는지 확인하십시오. 당신이 원하지 않거나 필요로하지 않는이 어리석은 표현 가능한 값이 있기 때문에이 미친 구현 선택과 생각할 것들.

널은 일반적으로 불필요한 복잡성을 추가합니다. 복잡성은 모든 소프트웨어의 적이므로 합리적 일 때마다 복잡성을 줄이려고 노력해야합니다.

(주에도 다음의 간단한 예를 더 복잡가 잘있다. A는해도 FirstName할 수 없다 null, A가 string나타낼 수 ""도 아마 우리가 모델로하고자하는 사람의 이름이 아닙니다 (빈 문자열). 이와 같이,도 비와 함께 nullable 문자열 인 경우에도 "무의미한 값을 나타내는"경우 일 수 있습니다. 다시, 런타임시 변하지 않는 코드와 조건부 코드를 통해 또는 유형 시스템 (예 : NonEmptyString유형) 을 사용하여이 문제를 해결하도록 선택할 수 있습니다 . 후자는 ( "좋은"유형의 일반적인 작업의 집합을 통해 종종 "폐쇄"이며, 예를 들면 아마 경솔한 인 NonEmptyString이상 닫혀 있지 않습니다.SubString(0,0))이지만 디자인 공간에서 더 많은 포인트를 보여줍니다. 하루가 끝날 때, 어떤 주어진 유형의 시스템에서는 제거하기에 매우 좋은 복잡성과 제거하기가 본질적으로 더 어려운 다른 복잡성이 있습니다. 이 주제의 핵심은 거의 모든 유형 시스템에서 "기본적으로 널 입력 가능 참조"에서 "기본적으로 널 입력 불가능 참조"로 변경되는 것은 거의 항상 간단한 변경으로, 유형 시스템이 복잡성과의 싸움에서 훨씬 더 우수하다는 것입니다. 특정 유형의 오류와 무의미한 상태를 배제합니다. 너무 많은 언어 가이 오류를 반복해서 반복한다는 것은 정말 미친 일입니다.)


31
다시 : 이름-실제로. 문이 열려 있지만 잠금 데드 볼트가 튀어 나와서 문이 닫히지 않는 문을 모델링하는 데주의를 기울일 수 있습니다. 세상에는 많은 복잡성이 있습니다. 핵심은 소프트웨어에서 "세계 상태"와 "프로그램 상태"간의 매핑을 구현할 때 복잡성을 하지 않는 것 입니다.
Brian

59
문을 열어 본 적이 없어요?
Joshua

58
사람들이 왜 특정 도메인의 의미론을 극복했는지 이해하지 못합니다. Brian은 결함을 간결하고 간단한 방식으로 null로 표시했습니다. 예, 모든 사람이 이름과 성을 가지고 있다고 말함으로써 문제 영역을 단순화했습니다. 질문은 'T', Brian에게 대답했습니다. 만약 당신이 보스턴에 있다면, 당신이 여기에서하는 모든 포스팅을 위해 맥주를 빚진 것입니다!
akaphenom

67
@akaphenom : 고마워하지만 모든 사람들이 맥주를 마시는 것은 아닙니다 (나는 술을 마시지 않습니다). 감사합니다 : P (실제 세계에서 너무 많은 복잡성! :))
Brian

4
이상하게도이 세상에는 3 개의 문이 있습니다! 그들은 일부 호텔에서 화장실 문으로 사용됩니다. 푸시 버튼은 내부의 키 역할을하며 외부에서 도어를 잠급니다. 래치 볼트가 움직이 자마자 자동으로 잠금 해제됩니다.
comonad

65

옵션 유형에 대한 좋은 점은 옵션 유형이 아니라는 것입니다. 그것은이다 다른 모든 유형하지 않습니다 .

때때로 , 우리는 일종의 "널 (null)"상태를 나타낼 수 있어야합니다. 때때로 우리는 변수가 취할 수있는 다른 가능한 값뿐만 아니라 "값 없음"옵션을 나타내야합니다. 따라서 평평하지 않은 언어는 이것이 약간 무너질 것입니다.

그러나 종종 , 우리는 그것을 필요로하지 않습니다 수 있도록 모호함과 혼란에 이러한 "널 (null)"상태에서만 리드를 : 나는 .NET에서 참조 형식 변수에 액세스 할 때마다, 나는 것을 고려해야 할 것이 null이 될 수 있습니다 .

프로그래머가 코드를 구성하여 코드 가 생성되지 않기 때문에 실제로 는 null 이되지 않는 경우가 종종 있습니다. 그러나 컴파일러는이를 확인할 수 없으며, 매번 볼 때마다 "이것은 null 일 수 있습니까? 여기서 null을 확인해야합니까?"

이상적으로 null이 의미가없는 많은 경우에는 허용되지 않아야합니다 .

거의 모든 것이 null 일 수있는 .NET에서는 달성하기 까다 롭습니다. 호출하는 코드의 작성자는 100 % 훈련을 받고 일관성을 유지하고 널이 될 수 있고없는 것이 무엇인지 명확하게 문서화했거나 편집증을 갖고 모든 것을 점검해야 합니다.

그러나 기본적으로 유형이 null을 허용 하지 않는 경우 null인지 여부를 확인할 필요가 없습니다. 컴파일러 / 유형 검사기가이를 위해 널을 적용 할 수 있으므로 널이 될 수 없다는 것을 알고 있습니다.

그런 다음 null 상태를 처리해야하는 드문 경우를 위해 백도어 필요합니다. 그런 다음 "옵션"유형을 사용할 수 있습니다. 그런 다음 "값이없는"경우를 나타낼 수 있어야한다는 의식적인 결정을 내린 경우에는 null을 허용하고 다른 모든 경우에는 값이 null이 아니라는 것을 알고 있습니다.

다른 사람들이 언급했듯이 예를 들어 C # 또는 Java에서 null은 다음 두 가지 중 하나를 의미 할 수 있습니다.

  1. 변수가 초기화되지 않았습니다. 이상적으로는 절대 일어나지 않아야합니다 . 변수 가 초기화 되지 않으면 존재 하지 않아야합니다.
  2. 변수에는 "선택적"데이터가 포함 됩니다 . 데이터가없는 경우를 나타낼 수 있어야합니다 . 때때로 필요합니다. 아마도 당신은 목록에서 객체를 찾으려고 할 것이고, 그것이 있는지 여부를 미리 알 수 없습니다. 그런 다음 "객체를 찾을 수 없음"을 나타낼 수 있어야합니다.

두 번째 의미는 유지되어야하지만 첫 번째 의미는 완전히 제거되어야합니다. 그리고 두 번째 의미조차도 기본값이되어서는 안됩니다. 필요할 때 언제 선택할 수 있습니다 . 그러나 선택 사항이 필요하지 않은 경우 유형 검사기 가 null이되지 않도록 보장 하기를 원합니다 .


그리고 두 번째 의미에서, 우리는 null을 먼저 확인하지 않고 그러한 변수에 액세스하려고하면 컴파일러가 경고하도록 (중지?) 원합니다. 여기에 곧 널 / null 이외의 C #을 기능에 대한 좋은 기사입니다 (결국은!) blogs.msdn.microsoft.com/dotnet/2017/11/15/...는
오핫 슈나이더

44

지금까지의 모든 대답은 왜 null나쁜 것이지, 그리고 언어가 특정 값이 null 이 되지 않도록 보장 할 수 있다면 얼마나 편리한 지에 중점을 둡니다 .

그런 다음 모든 값에 대해 Null을 허용하지 않으면 항상 깔끔한 아이디어가 될 것이라고 제안 합니다. 이는 개념을 추가 Option하거나 Maybe항상 정의 된 값을 가질 수없는 유형을 나타내는 경우 수행 할 수 있습니다 . 이것이 하스켈이 취한 접근법입니다.

모두 좋은 물건입니다! 그러나 동일한 효과를 얻기 위해 명시 적으로 nullable / null이 아닌 유형을 사용하는 것을 배제하지는 않습니다. 그렇다면 왜 옵션이 여전히 좋은가? (모든 후, 스칼라는 널 (NULL) 값을 지원합니다 가지고 있지만 지원은 자바 라이브러리로 작업 할 수 있도록,에) Options뿐만 아니라.

Q. 언어에서 널을 완전히 제거 할 수있는 것 이상의 이점은 무엇입니까?

A. 구성

널 인식 코드에서 순진한 번역을하는 경우

def fullNameLength(p:Person) = {
  val middleLen =
    if (null == p.middleName)
      p.middleName.length
    else
      0
  p.firstName.length + middleLen + p.lastName.length
}

옵션 인식 코드

def fullNameLength(p:Person) = {
  val middleLen = p.middleName match {
    case Some(x) => x.length
    case _ => 0
  }
  p.firstName.length + middleLen + p.lastName.length
}

큰 차이가 없습니다! 그러나 옵션을 사용 하는 끔찍한 방법 이기도합니다 ...이 접근법은 훨씬 깨끗합니다.

def fullNameLength(p:Person) = {
  val middleLen = p.middleName map {_.length} getOrElse 0
  p.firstName.length + middleLen + p.lastName.length
}

또는:

def fullNameLength(p:Person) =       
  p.firstName.length +
  p.middleName.map{length}.getOrElse(0) +
  p.lastName.length

옵션 목록을 다루기 시작하면 더 나아집니다. 목록 people자체가 선택 사항 이라고 상상해보십시오 .

people flatMap(_ find (_.firstName == "joe")) map (fullNameLength)

이것은 어떻게 작동합니까?

//convert an Option[List[Person]] to an Option[S]
//where the function f takes a List[Person] and returns an S
people map f

//find a person named "Joe" in a List[Person].
//returns Some[Person], or None if "Joe" isn't in the list
validPeopleList find (_.firstName == "joe")

//returns None if people is None
//Some(None) if people is valid but doesn't contain Joe
//Some[Some[Person]] if Joe is found
people map (_ find (_.firstName == "joe")) 

//flatten it to return None if people is None or Joe isn't found
//Some[Person] if Joe is found
people flatMap (_ find (_.firstName == "joe")) 

//return Some(length) if the list isn't None and Joe is found
//otherwise return None
people flatMap (_ find (_.firstName == "joe")) map (fullNameLength)

null 검사 (또는 elvis? : 연산자)가있는 해당 코드는 고통 스럽습니다. 여기서 실제 트릭은 flatMap 연산으로, nullable 값이 절대로 달성 할 수없는 방식으로 옵션과 컬렉션의 중첩 된 이해를 허용합니다.


8
+1, 이것은 강조하기에 좋은 포인트입니다. 하스켈-땅에서 이상 : 하나의 부록 flatMap호출 할 것이다 (>>=)모나드에 대한 인은 "바인딩"연산자. 맞습니다. Haskellers는 flatMap핑을 너무 좋아해서 언어 로고에 넣었습니다.
CA McCann

1
+1 다행히도의 표현 Option<T>은 결코 null이되지 않기를 바랍니다 . 슬프게도 스칼라는 uhh이며 여전히 Java와 연결되어 있습니다 :-) (반면, 스칼라가 Java와 잘 어울리지 않으면 누가 사용하겠습니까? Oo)

'List (null) .headOption'과 같이 쉽게 수행 할 수 있습니다. 이것은 반환 값 'None'과는 매우 다른 것을 의미합니다.
Kevin Wright

4
나는 당신이 작곡에 대해 말한 것을 좋아하기 때문에 당신에게 현상금을주었습니다. 다른 사람들은 언급하지 않은 것 같습니다.
Roman A. Taycher

훌륭한 예를 가진 훌륭한 답변!
thSoft

38

사람들이 그것을 놓치고있는 것처럼 보이기 때문에 : null모호하다.

앨리스의 생년월일은 null입니다. 무슨 뜻인가요?

밥의 사망일은 null입니다. 그게 무슨 뜻이야?

"합리적인"해석은 Alice의 생년월일은 존재하지만 알 수없는 반면 Bob의 생년월일은 존재하지 않는다는 것입니다 (밥은 여전히 ​​살아 있음). 그러나 왜 우리는 다른 대답을 얻었습니까?


또 다른 문제 null는 에지 케이스입니다.

  • 입니까 null = null?
  • 입니까 nan = nan?
  • 입니까 inf = inf?
  • 입니까 +0 = -0?
  • 입니까 +0/0 = -0/0?

답은 대개 "예", "아니오", "예", "예", "아니오", "예"입니다. 미치광이의 "수학자"는 NaN을 "무효 성 (nullity)"이라고 부르며 그것이 자신과 동등하다고 말합니다. SQL은 null을 다른 것과 같지 않은 것으로 취급하므로 NaN처럼 동작합니다. ± ∞, ± 0 및 NaN을 동일한 데이터베이스 열에 저장하려고 할 때 어떤 일이 발생하는지 궁금합니다 (2 개의 53 NaN이 있고 그 중 절반은 "음수"임).

설상가상으로, 데이터베이스는 NULL을 처리하는 방식이 다르며 대부분 일관성이 없습니다 ( 개요 는 SQLite의 NULL 처리 참조 ). 꽤 끔찍하다.


그리고 의무적 인 이야기를 위해 :

최근에 5 개의 열이있는 (sqlite3) 데이터베이스 테이블을 설계했습니다 a NOT NULL, b, id_a, id_b NOT NULL, timestamp. 상당히 임의적 인 앱의 일반적인 문제를 해결하기 위해 설계된 일반적인 스키마이기 때문에 두 가지 고유 제약 조건이 있습니다.

UNIQUE(a, b, id_a)
UNIQUE(a, b, id_b)

id_a기존 앱 디자인과의 호환성을 위해서만 존재하며 (부분적으로 더 나은 솔루션을 찾지 못했기 때문에) 새로운 앱에는 사용되지 않습니다. SQL에서 NULL이 작동하는 방식 때문에 첫 번째 고유성 제약 조건을 삽입 (1, 2, NULL, 3, t)하고 (1, 2, NULL, 4, t)위반하지 않을 수 있습니다 ( (1, 2, NULL) != (1, 2, NULL)).

이것은 대부분의 데이터베이스에서 고유 제약 조건에서 NULL이 작동하는 방식 때문에 특히 작동합니다 (아마도 "실제"상황을 모델링하는 것이 더 쉽습니다 (예 : 두 사람이 동일한 사회 보장 번호를 가질 수는 있지만 모든 사람이 하나는 아님)).


FWIW는 먼저 정의되지 않은 동작을 호출하지 않으면 C ++ 참조가 널을 "지점"할 수 없으며 초기화되지 않은 참조 멤버 변수를 사용하여 클래스를 구성 할 수 없습니다 (예외가 발생하면 구성이 실패 함).

참고 : 때로는 가상의 iOS와 같이 상호 배타적 인 포인터를 원할 수도 있습니다 (예 : 하나만 NULL이 아닐 수 있음) type DialogState = NotShown | ShowingActionSheet UIActionSheet | ShowingAlertView UIAlertView | Dismissed. 대신, 나는 다음과 같은 일을 강요 assert((bool)actionSheet + (bool)alertView == 1)받습니다.


실제 수학자들은 "NaN"이라는 개념을 사용하지는 않지만 안심하십시오.
Noldorin

@Noldorin : 그들은하지만 "불확정 한 형태"라는 용어를 사용합니다.
IJ 케네디

@IJKennedy : 다른 대학입니다. 감사합니다. 일부 'NaN'은 불확실한 형태를 나타낼 수 있지만 FPA는 상징적 추론을 수행하지 않기 때문에 불확실한 형태와 동일시하는 것은 오해의 소지가 있습니다!
Noldorin

무슨 일이야 assert(actionSheet ^ alertView)? 아니면 당신의 언어 XOR이 바보가 될 수 없습니까?
고양이

16

참조 / 포인터를 갖는 바람직하지 않은 것은 기본적으로 널 입력 가능합니다.

이것이 이것이 null의 주요 문제라고 생각하지 않습니다. null의 주요 문제는 두 가지를 의미 할 수 있다는 것입니다.

  1. 참조 / 포인터가 초기화되지 않았습니다. 여기서 문제는 일반적으로 변경 가능성과 동일합니다. 우선, 코드를 분석하기가 더 어렵습니다.
  2. 변수가 null 인 것은 실제로 무언가를 의미합니다. 옵션 유형이 실제로 형식화되는 경우입니다.

옵션 유형을 지원하는 언어는 일반적으로 초기화되지 않은 변수의 사용도 금지하거나 권장하지 않습니다.

패턴 일치와 같은 null 사례를 쉽게 확인할 수있는 전략을 포함한 옵션 유형의 작동 방식

효과적이려면 옵션 유형이 언어로 직접 지원되어야합니다. 그렇지 않으면 시뮬레이션하기 위해 많은 보일러 플레이트 코드가 필요합니다. 패턴 일치 및 유형 유추는 옵션 유형을 쉽게 처리 할 수있는 두 가지 핵심 언어 기능입니다. 예를 들면 다음과 같습니다.

F #에서 :

//first we create the option list, and then filter out all None Option types and 
//map all Some Option types to their values.  See how type-inference shines.
let optionList = [Some(1); Some(2); None; Some(3); None]
optionList |> List.choose id //evaluates to [1;2;3]

//here is a simple pattern-matching example
//which prints "1;2;None;3;None;".
//notice how value is extracted from op during the match
optionList 
|> List.iter (function Some(value) -> printf "%i;" value | None -> printf "None;")

그러나 옵션 유형을 직접 지원하지 않는 Java와 같은 언어에서는 다음과 같은 것이 있습니다.

//here we perform the same filter/map operation as in the F# example.
List<Option<Integer>> optionList = Arrays.asList(new Some<Integer>(1),new Some<Integer>(2),new None<Integer>(),new Some<Integer>(3),new None<Integer>());
List<Integer> filteredList = new ArrayList<Integer>();
for(Option<Integer> op : list)
    if(op instanceof Some)
        filteredList.add(((Some<Integer>)op).getValue());

메시지를 먹는 것과 같은 대체 솔루션

Objective-C의 "Message eating nil"은 null 점검의 두통을 줄이기위한 해결책이 아닙니다. 기본적으로 null 개체에서 메서드를 호출하려고 할 때 런타임 예외가 발생하지 않고 대신 식 자체가 null로 평가됩니다. 불신을 중단하는 것은 마치 각 인스턴스 메소드가로 시작하는 것과 같습니다 if (this == null) return null;. 그러나 정보 손실이 있습니다. 메서드가 유효한 반환 값이거나 개체가 실제로 null이기 때문에 null을 반환했는지 여부를 알 수 없습니다. 예외 삼키는 것과 비슷하며 이전에 null로 설명 된 문제를 해결하는 데 아무런 진전이 없습니다.


이것은 애완 동물이지만 C #은 거의 C와 같은 언어가 아닙니다.
Roman A. Taycher

4
C #이 더 나은 솔루션을 가지고 있기 때문에 Java를 사용하려고했습니다 ...하지만 사람들이 정말로 의미하는 것은 "c에서 영감을 얻은 구문을 가진 언어"입니다. 계속해서 "c-like"문장을 교체했습니다.
Stephen Swensen

linq와 함께, 맞습니다. 나는 C #을 생각하고 있었고 그것을 알지 못했습니다.
Roman A. Taycher

1
c에서 영감을 얻은 구문이 대부분 가능하지만 함수 프로그래머가 c와 같은 구문을 말하는 c와 비슷한 방식으로 파이썬 / 루비와 같은 명령형 프로그래밍 언어에 대해서도 들었습니다.
Roman A. Taycher

11

어셈블리는 타입이 지정되지 않은 포인터로 알려진 주소를 가져 왔습니다. C는 그것들을 타입 포인터로 직접 매핑했지만 Algol의 null을 모든 타입 포인터와 호환되는 고유 포인터 값으로 소개했습니다. C에서 null의 큰 문제는 모든 포인터가 null 일 수 있으므로 수동 검사없이 포인터를 안전하게 사용할 수 없다는 것입니다.

고급 언어에서 null을 갖는 것은 실제로 두 가지 다른 개념을 전달하기 때문에 어색합니다.

  • 무언가가 정의되어 있지 않다고 말하는 것 .
  • 무언가를 말하는 것은 선택 사항 입니다.

정의되지 않은 변수를 갖는 것은 거의 쓸모가 없으며 변수가 발생할 때마다 정의되지 않은 동작이 발생합니다. 모든 사람들이 정의되지 않은 것을 갖는 것은 모든 비용으로 피해야한다고 동의 할 것입니다.

두 번째 경우는 선택 사항이며 옵션 유형 과 같이 명시 적으로 제공하는 것이 가장 좋습니다 .


운송 회사에 있으며 운전사 일정을 잡는 데 도움이되는 응용 프로그램을 만들어야한다고 가정 해 보겠습니다. 각 운전자에 대해 다음과 같은 몇 가지 정보를 저장합니다. 즉, 운전 면허증과 비상시 전화 번호입니다.

C에서는 다음을 가질 수 있습니다.

struct PhoneNumber { ... };
struct MotorbikeLicence { ... };
struct CarLicence { ... };
struct TruckLicence { ... };

struct Driver {
  char name[32]; /* Null terminated */
  struct PhoneNumber * emergency_phone_number;
  struct MotorbikeLicence * motorbike_licence;
  struct CarLicence * car_licence;
  struct TruckLicence * truck_licence;
};

관찰 한 바와 같이 드라이버 목록을 처리 할 때 null 포인터를 확인해야합니다. 컴파일러는 도움이되지 않습니다. 프로그램의 안전은 어깨에 달려 있습니다.

OCaml에서 동일한 코드는 다음과 같습니다.

type phone_number = { ... }
type motorbike_licence = { ... }
type car_licence = { ... }
type truck_licence = { ... }

type driver = {
  name: string;
  emergency_phone_number: phone_number option;
  motorbike_licence: motorbike_licence option;
  car_licence: car_licence option;
  truck_licence: truck_licence option;
}

이제 트럭 운전사 번호와 함께 모든 운전자의 이름을 인쇄하고 싶다고 가정 해 봅시다.

C에서 :

#include <stdio.h>

void print_driver_with_truck_licence_number(struct Driver * driver) {
  /* Check may be redundant but better be safe than sorry */
  if (driver != NULL) {
    printf("driver %s has ", driver->name);
    if (driver->truck_licence != NULL) {
      printf("truck licence %04d-%04d-%08d\n",
        driver->truck_licence->area_code
        driver->truck_licence->year
        driver->truck_licence->num_in_year);
    } else {
      printf("no truck licence\n");
    }
  }
}

void print_drivers_with_truck_licence_numbers(struct Driver ** drivers, int nb) {
  if (drivers != NULL && nb >= 0) {
    int i;
    for (i = 0; i < nb; ++i) {
      struct Driver * driver = drivers[i];
      if (driver) {
        print_driver_with_truck_licence_number(driver);
      } else {
        /* Huh ? We got a null inside the array, meaning it probably got
           corrupt somehow, what do we do ? Ignore ? Assert ? */
      }
    }
  } else {
    /* Caller provided us with erroneous input, what do we do ?
       Ignore ? Assert ? */
  }
}

OCaml에서는 다음과 같습니다.

open Printf

(* Here we are guaranteed to have a driver instance *)
let print_driver_with_truck_licence_number driver =
  printf "driver %s has " driver.name;
  match driver.truck_licence with
    | None ->
        printf "no truck licence\n"
    | Some licence ->
        (* Here we are guaranteed to have a licence *)
        printf "truck licence %04d-%04d-%08d\n"
          licence.area_code
          licence.year
          licence.num_in_year

(* Here we are guaranteed to have a valid list of drivers *)
let print_drivers_with_truck_licence_numbers drivers =
  List.iter print_driver_with_truck_licence_number drivers

이 간단한 예제에서 볼 수 있듯이 안전한 버전에는 복잡한 것이 없습니다.

  • 터져 요.
  • 훨씬 더 나은 보증을받으며 null 검사가 전혀 필요하지 않습니다.
  • 컴파일러는 옵션을 올바르게 처리했는지 확인했습니다.

C에서는 널 검사와 붐을 잊어 버릴 수 있습니다 ...

참고 :이 코드는 컴파일되지 않은 샘플이지만 아이디어를 얻길 바랍니다.


나는 그것을 시도하지 않았지만 en에 대해 null이 아닌 포인터를 허용하기 위해 en.wikipedia.org/wiki/Cyclone_%28programming_language%29 주장.
Roman A. Taycher

1
나는 아무도 첫 번째 사건에 관심이 없다는 당신의 진술에 동의하지 않습니다. 많은 사람들, 특히 기능적 언어 공동체의 사람들은 이것에 매우 관심이 있으며 초기화되지 않은 변수의 사용을 권장하지 않거나 완전히 금지합니다.
Stephen Swensen

나는 NULL"아무것도 가리킬 수없는 참조"가 일부 Algol 언어에 대해 발명 되었다고 생각 합니다 (Wikipedia는 동의합니다 . en.wikipedia.org/wiki/Null_pointer#Null_pointer 참조 ). 그러나 물론 어셈블리 프로그래머는 포인터를 유효하지 않은 주소로 초기화했을 가능성이 높습니다 (읽기 : Null = 0).

1
@Stephen : 우리는 아마 같은 것을 의미했습니다. 저에게 그들은 초기화되지 않은 것들의 사용을 권장하지 않으며 금지합니다. 왜냐하면 우리가 그들과 함께 제정신이 있거나 유용한 것을 할 수 없기 때문에 정의되지 않은 것들에 대해 논의 할 이유가 없기 때문입니다. 그것은 전혀 관심이 없습니다.
bltxd

2
@tc. null은 어셈블리와 관련이 없다고 말합니다. 어셈블리에서 형식은 일반적으로 null을 허용 하지 않습니다 . 범용 레지스터에로드 된 값은 0이거나 0이 아닌 정수일 수 있습니다. 그러나 결코 null 일 수 없습니다. 메모리 주소를 레지스터에로드하더라도 대부분의 일반적인 아키텍처에서는 "널 포인터"에 대한 별도의 표현이 없습니다. 그것은 C와 같은 고급 언어로 도입 된 개념입니다.
jalf

5

Microsoft Research에는 다음과 같은 흥미로운 프로젝트가 있습니다.

투기#

Null아닌 유형을 가진 C # 확장이며 null아닌 객체검사 하는 메커니즘 이지만 IMHO 는 계약 원칙에 따라 디자인을 적용하는 것이 null 참조로 인한 많은 까다로운 상황에 더 적합하고 도움이 될 수 있습니다.


4

.NET 배경에서 나온 null은 항상 유용한 점이 있다고 생각했습니다. 내가 구조체와 많은 상용구 코드를 피하는 것이 얼마나 쉬운 지 알게 될 때까지. Tony Hoare 는 2009 년 QCon London에서 연설 하면서 null 참조를 발명 한 것에 대해 사과했습니다 . 그를 인용하려면 :

나는 그것을 10 억 달러의 실수라고 부릅니다. 그것은 1965 년에 null 참조의 발명이었다. 당시 나는 객체 지향 언어 (ALGOL W)로 참조 할 수있는 최초의 포괄적 인 타입 시스템을 설계하고 있었다. 필자의 목표는 컴파일러가 자동으로 검사를 수행하여 모든 참조 사용이 절대적으로 안전하도록하는 것이 었습니다. 그러나 구현하기가 쉽기 때문에 null 참조를 넣는 유혹에 저항 할 수 없었습니다. 이로 인해 수많은 오류, 취약성 및 시스템 충돌이 발생했으며, 이는 아마도 지난 40 년 동안 수십억 달러의 고통과 피해를 야기했을 것입니다. 최근에는 Microsoft의 PREfix 및 PREfast와 같은 많은 프로그램 분석기가 참조를 확인하는 데 사용되었으며 Null이 아닌 위험이있는 경우 경고를 표시합니다. Spec #과 같은 최신 프로그래밍 언어는 널이 아닌 참조에 대한 선언을 도입했습니다. 이것은 1965 년에 거부 한 해결책입니다.

프로그래머 도이 질문을 참조하십시오



1

나는 항상 Null (또는 nil)을 값이없는 것으로 보았습니다 .

때때로 당신은 이것을 원하고 때로는 원하지 않습니다. 작업중인 도메인에 따라 다릅니다. 부재가 의미있는 경우 : 중간 이름이 없으면 응용 프로그램이 그에 따라 작동 할 수 있습니다. 반면에 null 값이 없어야하는 경우 : 이름이 null이면 개발자는 잠정적 인 오전 2시 전화를받습니다.

또한 코드가 오버로드되고 null 검사로 지나치게 복잡 해지는 것을 보았습니다. 나에게 이것은 두 가지 중 하나를 의미합니다 :
a) 응용 프로그램 트리에서 더 높은 버그
b) 잘못되었거나 불완전한 디자인

긍정적 인 측면에서-Null은 아마도 무언가가 없는지 확인하는 데 더 유용한 개념 중 하나이며 null 개념이없는 언어는 데이터 유효성 검사를 수행 할 때 지나치게 복잡해집니다. 이 경우, 새로운 변수가 초기화되지 않으면, 상기 언어는 변수를 빈 문자열, 0 또는 빈 컬렉션으로 설정합니다. 그러나 빈 문자열 또는 0 또는 빈 컬렉션이 응용 프로그램에 유효한 값 이면 문제가 있습니다.

때로는 초기화되지 않은 상태를 나타 내기 위해 필드에 특수 / 이상한 값을 발명하여 우회했습니다. 그렇다면 의도가 좋은 사용자가 특별 가치를 입력하면 어떻게됩니까? 그리고 이것이 데이터 유효성 검사 루틴을 만들게 될 혼란에 빠지지 마십시오. 언어가 널 (NULL) 개념을 지원하면 모든 문제가 사라질 것입니다.


안녕하세요 @Jon, 여기 당신을 따라 가기가 조금 어렵습니다. 마지막으로 "특별 / 이상한"값으로 Javascript의 '정의되지 않은'또는 IEEE의 'NaN'과 같은 것을 의미한다는 것을 깨달았습니다. 그러나 그 외에도 OP가 요청한 질문은 실제로 다루지 않습니다. 그리고 "아무것도 없는지 확인하기 위해 Null이 아마도 가장 유용한 개념"이라는 진술은 거의 확실하지 않습니다. 옵션 유형은 null에 대한 잘 알려진 유형 안전 대안입니다.
Stephen Swensen

@Stephen-실제로 내 메시지를 되돌아 보면, 후반부 전체가 아직 묻지 않은 질문으로 옮겨 져야한다고 생각합니다. 그러나 여전히 null은 무언가가 없는지 확인하는 데 매우 유용하다고 말합니다.
Jon

0

벡터 언어는 null이없는 경우도 있습니다.

이 경우 빈 벡터는 유형이 지정된 null로 사용됩니다.


나는 당신이 무슨 말을하는지 이해하지만 몇 가지 예를 나열 할 수 있습니까? 특히 여러 함수를 null 값에 적용하는 경우?
Roman A. Taycher

빈 벡터에 벡터 변환을 적용하면 다른 빈 벡터가 생성됩니다. 참고로 SQL은 대부분 벡터 언어입니다.
Joshua

1
알았어요. SQL은 행의 벡터 언어이고 열의 값 언어입니다.
Joshua
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.