프로그래머가 널 오류 / 예외에 대해 불평 할 때마다 누군가가 널없이 우리가 무엇을하는지 묻습니다.
나는 옵션 유형의 차가움에 대한 기본 아이디어를 가지고 있지만 그것을 표현할 수있는 지식이나 언어 기술이 없습니다. 일반 프로그래머가 접근 할 수있는 방식으로 작성된 다음에 대한 훌륭한 설명은 무엇입니까 ?
- 참조 / 포인터가 바람직하지 않은 경우 기본적으로 널 입력 가능
- 다음과 같은 null 사례를 쉽게 확인할 수있는 전략을 포함한 옵션 유형 작동 방식
- 패턴 매칭
- 수도원 이해
- 메시지를 먹는 것과 같은 대체 솔루션
- (내가 놓친 다른 측면들)
답변
나는 왜 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
을 가지고 있지만 일부 사람들 만 MiddleName
s를 가지고 있다면
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)
)이지만 디자인 공간에서 더 많은 포인트를 보여줍니다. 하루가 끝날 때, 어떤 주어진 유형의 시스템에서는 제거하기에 매우 좋은 복잡성과 제거하기가 본질적으로 더 어려운 다른 복잡성이 있습니다. 이 주제의 핵심은 거의 모든 유형 시스템에서 “기본적으로 널 입력 가능 참조”에서 “기본적으로 널 입력 불가능 참조”로 변경되는 것은 거의 항상 간단한 변경으로, 유형 시스템이 복잡성과의 싸움에서 훨씬 더 우수하다는 것입니다. 특정 유형의 오류와 무의미한 상태를 배제합니다. 너무 많은 언어 가이 오류를 반복해서 반복한다는 것은 정말 미친 일입니다.)
답변
옵션 유형에 대한 좋은 점은 옵션 유형이 아니라는 것입니다. 그것은이다 다른 모든 유형하지 않습니다 .
때때로 , 우리는 일종의 “널 (null)”상태를 나타낼 수 있어야합니다. 때때로 우리는 변수가 취할 수있는 다른 가능한 값뿐만 아니라 “값 없음”옵션을 나타내야합니다. 따라서 평평하지 않은 언어는 이것이 약간 무너질 것입니다.
그러나 종종 , 우리는 그것을 필요로하지 않습니다 수 있도록 모호함과 혼란에 이러한 “널 (null)”상태에서만 리드를 : 나는 .NET에서 참조 형식 변수에 액세스 할 때마다, 나는 것을 고려해야 할 것이 null이 될 수 있습니다 .
프로그래머가 코드를 구성하여 코드 가 생성되지 않기 때문에 실제로 는 null 이되지 않는 경우가 종종 있습니다. 그러나 컴파일러는이를 확인할 수 없으며, 매번 볼 때마다 “이것은 null 일 수 있습니까? 여기서 null을 확인해야합니까?”
이상적으로 null이 의미가없는 많은 경우에는 허용되지 않아야합니다 .
거의 모든 것이 null 일 수있는 .NET에서는 달성하기 까다 롭습니다. 호출하는 코드의 작성자는 100 % 훈련을 받고 일관성을 유지하고 널이 될 수 있고없는 것이 무엇인지 명확하게 문서화했거나 편집증을 갖고 모든 것을 점검해야 합니다.
그러나 기본적으로 유형이 null을 허용 하지 않는 경우 null인지 여부를 확인할 필요가 없습니다. 컴파일러 / 유형 검사기가이를 위해 널을 적용 할 수 있으므로 널이 될 수 없다는 것을 알고 있습니다.
그런 다음 null 상태를 처리해야하는 드문 경우를 위해 백도어 가 필요합니다. 그런 다음 “옵션”유형을 사용할 수 있습니다. 그런 다음 “값이없는”경우를 나타낼 수 있어야한다는 의식적인 결정을 내린 경우에는 null을 허용하고 다른 모든 경우에는 값이 null이 아니라는 것을 알고 있습니다.
다른 사람들이 언급했듯이 예를 들어 C # 또는 Java에서 null은 다음 두 가지 중 하나를 의미 할 수 있습니다.
- 변수가 초기화되지 않았습니다. 이상적으로는 절대 일어나지 않아야합니다 . 변수 가 초기화 되지 않으면 존재 하지 않아야합니다.
- 변수에는 “선택적”데이터가 포함 됩니다 . 데이터가없는 경우를 나타낼 수 있어야합니다 . 때때로 필요합니다. 아마도 당신은 목록에서 객체를 찾으려고 할 것이고, 그것이 있는지 여부를 미리 알 수 없습니다. 그런 다음 “객체를 찾을 수 없음”을 나타낼 수 있어야합니다.
두 번째 의미는 유지되어야하지만 첫 번째 의미는 완전히 제거되어야합니다. 그리고 두 번째 의미조차도 기본값이되어서는 안됩니다. 필요할 때 언제 선택할 수 있습니다 . 그러나 선택 사항이 필요하지 않은 경우 유형 검사기 가 null이되지 않도록 보장 하기를 원합니다 .
답변
지금까지의 모든 대답은 왜 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 값이 절대로 달성 할 수없는 방식으로 옵션과 컬렉션의 중첩 된 이해를 허용합니다.
답변
사람들이 그것을 놓치고있는 것처럼 보이기 때문에 : 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)
받습니다.
답변
참조 / 포인터를 갖는 바람직하지 않은 것은 기본적으로 널 입력 가능합니다.
이것이 이것이 null의 주요 문제라고 생각하지 않습니다. null의 주요 문제는 두 가지를 의미 할 수 있다는 것입니다.
- 참조 / 포인터가 초기화되지 않았습니다. 여기서 문제는 일반적으로 변경 가능성과 동일합니다. 우선, 코드를 분석하기가 더 어렵습니다.
- 변수가 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는 그것들을 타입 포인터로 직접 매핑했지만 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에서는 널 검사와 붐을 잊어 버릴 수 있습니다 …
참고 :이 코드는 컴파일되지 않은 샘플이지만 아이디어를 얻길 바랍니다.
답변
Microsoft Research에는 다음과 같은 흥미로운 프로젝트가 있습니다.
투기#
Null 이 아닌 유형을 가진 C # 확장이며 null 이 아닌 객체 를 검사 하는 메커니즘 이지만 IMHO 는 계약 원칙에 따라 디자인을 적용하는 것이 null 참조로 인한 많은 까다로운 상황에 더 적합하고 도움이 될 수 있습니다.