В данной статье мы расскажем об особенностях объекта nil.
Сразу стоит отметить, что nil из Smalltalk и null в традиционных языках, это далеко не одно и то же. Ключевое слово nil ссылается на синглтон класса UndefinedObject, наследника Object. То есть, он является полноценным объектом и может отвечать на любые сообщения, для которых есть реализации в классах Object и UndefinedObject.
Такая реализация контрастирует с семейством C-подобных языков, где null или (void*)0 или (TYPE*)0 обозначает "ничто". Под этим определением не скрывается никакого реального объекта, оно лишь обозначает, что данное значение не содержит корректной ссылки на объект заданного типа и может участвовать лишь в проверках с другими ссылками. Таким образом переменную, содержащую null, можно использовать в качестве параметра, но никогда в качестве объекта-получателя сообщения, иначе возникнет NullPointerException либо неопределенное поведение.
То есть, фактически, каждая ссылочная переменная имеет тип не просто <Type>, а <Type | null>. Поэтому теоретически любая переменная может стать причиной исключения в отсутствие проверки вида
if (var != null)
В частности, на практике часто возникает проблема при использовании следующей конструкции
var1.equals(var2)
Хотя было бы логично, чтобы null соответствующим образом отвечал на сообщение equals, иначе приходится писать вот так
var1 == var2 || (var1 != null && var1.equals(var2))
либо определить статический метод, реализующий данную проверку, либо гарантировать, что первая ссылка заведомо не содержит null.
Некоторые встроенные конструкции содержат обход указанной проблемы, например
var instanceof SomeClass
Данный оператор уже содержит встроенную проверку на null, но этот прием доступен только разработчикам языка.
Поэтому становится очевидным почему оператор instanceof (и ряд других) не может быть реализован в таком виде как метод экземпляра класса Object. Одна нелогичность явилась результатом появления других.
Но вернемся опять к Smalltalk и объекту nil. Так как UndefinedObject является прямым наследником Object, то он содержит всю базовую функциональность, его можно использовать в выражениях, и как параметр, и как получатель сообщения.
Например эти вызовы являются безопасными:
nil = nil вернет true nil = 'a string' вернет false nil class вернет UndefinedObject nil printString вернет 'nil'
В некотором смысле Smalltalk является более "безопасным" по отношению к языкам, в которых любое обращение к null вызывает исключение.
Благодаря этому также возникла идиома, по которой сама проверка заменяется на сообщение:
self someCondition isNil ifTrue: [...] nil isNil вернет true nil notNil вернет false Object new isNil вернет false Object new notNil вернет true
Также стало возможным реализовать упрощенную условную конструкцию (как всегда, в виде обычного метода), заменяющую проверку на nil:
result := self someCondition ifNil: ['nil value'] ifNotNil: [:obj | 'non-nil value: ' , obj printString]
Например, следующим образом может быть реализован Singleton:
Singleton class>>instance ^instance ifNil: [instance := self new]
ВЫВОДЫ
Данная статья демонстрирует то, как язык, построенный на минимальном количестве базовых концепций, доступных в одинаковой мере и создателям языка и разработчикам, порождает однородный дизайн с предсказуемым поведением. В данном случае, даже такое обособленное понятие, как nil, ничем особенным не выделяется (кроме его специфической роли на уровне VM как начальное значение неинициализированных данных) и повинуется общим правилам для всех остальных объектов. Меньшее количество частных случаев дает большие возможности для разработчика, большую предсказуемость, а значит безопасность.
По данной теме у нас на сайте уже имеется статья, описывающее возможное изменение семантики обычного языка для поддержки полноценного объекта null - Null Considered Harmful?
Также имеется перевод статьи Обобщенный паттерн <Null Object>, в которой рассматривается реализация Null Object, "поедающая" сообщения.