Я думаю, что краткое резюме того, почему ноль нежелателен, состоит в том, что бессмысленные состояния не должны быть представимыми .
Предположим, я моделирую дверь. Он может находиться в одном из трех состояний: открыто, закрыто, но разблокировано, закрыто и заблокировано. Теперь я мог бы смоделировать его в соответствии с
class Door
private bool isShut
private bool isLocked
и ясно, как отобразить мои три состояния в эти две логические переменные. Но это оставляет четвертое, нежелательное состояние доступно: isShut==false && isLocked==true
. Поскольку типы, которые я выбрал в качестве своего представления, допускают это состояние, я должен приложить умственные усилия, чтобы гарантировать, что класс никогда не попадет в это состояние (возможно, путем явного кодирования инварианта). Напротив, если бы я использовал язык с алгебраическими типами данных или проверенные перечисления, которые позволяют мне определить
type DoorState =
| Open | ShutAndUnlocked | ShutAndLocked
тогда я мог бы определить
class Door
private DoorState state
и больше нет забот. Система типов гарантирует, что существует только три возможных состояния для экземпляра class Door
. Это то, к чему хорошо подходят системы типов - явное исключение целого класса ошибок во время компиляции.
Проблема в null
том, что каждый ссылочный тип получает это дополнительное состояние в своем пространстве, которое обычно нежелательно. string
Переменная может быть любая последовательность символов, или это может быть этот сумасшедший дополнительное null
значение , которое не отображает в моей проблемной области. У Triangle
объекта есть три Point
s, у которых есть X
и Y
значения, и , к сожалению, сами Point
по Triangle
себе или s могут быть этим сумасшедшим нулевым значением, которое не имеет смысла для графической области, в которой я работаю. И т.д.
Если вы намереваетесь смоделировать потенциально несуществующее значение, вы должны явно указать его. Если я собираюсь смоделировать людей так, чтобы у каждого Person
были a FirstName
и a LastName
, но только у некоторых людей есть MiddleName
s, то я хотел бы сказать что-то вроде
class Person
private string FirstName
private Option<string> MiddleName
private string LastName
где string
здесь предполагается ненулевой тип. Тогда нет никаких хитрых инвариантов для установления и никаких неожиданных NullReferenceException
s при попытке вычислить длину чьего-либо имени. Система типов гарантирует, что любой код, имеющий дело с 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
без забот. Напротив, в языке с обнуляемыми ссылками для таких типов, как строка, тогда предполагается
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 не имеет инварианта всего, что является ненулевым, или
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
гарантирует наличие первого / последнего, но середина может быть нулевой, или, может быть, вы делаете проверки, которые выдают различные типы исключений, или кто знает что. Все эти сумасшедшие варианты реализации и вещи, о которых нужно подумать, возникают, потому что есть эта глупая представимая ценность, которая вам не нужна или не нужна.
Нуль обычно добавляет ненужную сложность. Сложность - враг всего программного обеспечения, и вы должны стремиться уменьшать сложность всякий раз, когда это целесообразно.
(Обратите внимание, что даже эти простые примеры сложнее. Даже если a FirstName
не может быть null
, a string
может представлять ""
(пустую строку), что, вероятно, также не является личным именем, которое мы намереваемся смоделировать. Таким образом, даже с не обнуляемые строки, все равно может случиться так, что мы «представляем бессмысленные значения». Опять же, вы можете решить бороться с этим либо с помощью инвариантов и условного кода во время выполнения, либо с помощью системы типов (например, чтобы иметь NonEmptyString
тип). последний, вероятно, опрометчив («хорошие» типы часто «закрываются» по набору общих операций и, например NonEmptyString
, не закрываются по.SubString(0,0)
), но это демонстрирует больше точек в пространстве дизайна. В конце концов, в любой системе типов есть некоторая сложность, от которой будет очень хорошо избавиться, и другая сложность, от которой просто сложнее избавиться. Ключевым моментом в этом разделе является то, что почти во всех системах типов изменение с «необнуляемых ссылок по умолчанию» на «необнуляемые ссылки по умолчанию» почти всегда является простым изменением, которое делает систему типов намного лучше в борьбе со сложностью и исключение определенных типов ошибок и бессмысленных состояний. Поэтому довольно безумно, что многие языки повторяют эту ошибку снова и снова.)