Nullsafe оператор на практике
null
. Он имеет большую привлекательность, поскольку называется "безопасным" и сокращает количество проверок перед вызовом метода. Однако на практике у оператора nullsafe есть и свои недостатки.- Nullsafe оператор
- Проверьте до, проверьте после: просто проверьте!
- Какой это
null
? - Типизация для nullsafe оператора
- Nullsafe оператор на практике со статическим анализом
Первый недостаток — проверка, которая была до вызова метода, на самом деле должна быть после него: она всё ещё необходима. Второй — типы, используемые в выражении, теперь должны избегать типа null
. Как обычно, новый синтаксис означает новую организацию вокруг него. Выберите свою сторону.
Давайте рассмотрим его вместе.
Nullsafe оператор
Nullsafe оператор ?->
близкий родственник объектного оператора ->
, с которым он имеет общую стрелочную конструкцию. Разница заключается в обработке null
, когда он используется в качестве объектной части синтаксиса.
<?php
$x = null;
$x?->foo();
// Ничего не происходит
$x->foo();
// Fatal error: Uncaught Error: Call to a member function foo() on null in
?>
Это работает только для значений null
: нет никакой разницы при вызове метода для целого числа или массива: Фатальная ошибка.
Первое, на что влияет этот оператор, — предотвращение досадной остановки выполнения только потому, что объект недоступен. Существует бесчисленное множество методов, которые возвращают объект или null
в случае неудачи с поиском информации. Такие методы требуют дополнительной проверки результата, и оператор nullsafe как раз для этого и предназначен.
<?php
function getInstance($what) : ?What {}
$x = getInstance('what');
$x?->foo();
// foo() выполняется на What, или ничего не происходит.
$x->foo();
// Фатальная ошибка, если getInstance() возвращает null
// безопасная версия вышеописанного, с дополнительным кодом
if ($x !== null) {
$x->foo();
}
?>
Отсюда и название оператора: он безопасен, так как не блокирует выполнение, если значение null
умудрится найти свой путь. Второе преимущество — краткость написания кода: когда в коде возникают ошибки из-за значения null
, достаточно добавить вопросительный знак ?
, для решения проблемы.
Проверьте до, проверьте после: просто проверьте!
При использовании nullsafe оператора объект, являющийся null
, отменяет всё выражение: получается результат, который сам является null
. История умалчивает, что это тот же null
, что и объект, и это остаётся на усмотрение читателя.
Поскольку выполнение продолжается, это делает выражение nullsafe необязательным. Это может быть вызов "fire-and-forget", когда вызов делает что-то, что не имеет локального влияния, например, запись в лог; это может быть необязательный вызов или вызов, который безопасно завершается. Это может быть функция, возвращающая void
, но к этому случаю мы вернёмся позже.
С другой стороны, когда выражение должно вернуть значение, его необходимо проверить на соответствие null
. Помните, что перед выполнением никакой проверки объекта не проводилось.
<?php
function getInstance($what) : ?What {}
$result = getInstance('what')?->foo();
// Проверка результата выражения.
if ($result === null) {
manageError();
}
// иначе продолжайте исполнение, всё хорошо!
?>
Первое замечание — это проверка на null
. При использовании ->
проверка выполняется до вызова, и возвращаемое значение — это только результат. При использовании ?->
проверка производится после вызова, по возвращаемому значению: результатом может быть любое допустимое значение или null
в случае, если это невозможно.
В итоге проверка не была удалена, а для обеспечения бесперебойного выполнения осталось добавить дополнительный код.
Какой это null
?
Второе замечание заключается в том, что код не может отличить null
, потому что в выражении указан неправильный объект, или null
, являющийся корректным возвращаемым значением. Рассмотрим метод foo
, определённый следующим образом:
<?php
class What {
function foo() : ?int {}
}
function getInstance($what) : ?What {}
$result = getInstance('what')?->foo();
$result = null?->foo();
// Это null из-за foo? или из-за getInstance()?
if ($result === null) {
manageError();
}
?>
Традиционно null
— это признак ошибки: мы попросили getInstance()
и foo сделать что-то, и, по крайней мере, один из них не справился. В обоих случаях в меню должно быть управление ошибками.
Точнее, код не может определить источник ошибок, поскольку обе они равны null
. Чтобы обнаружить источник, нужен дополнительный шаг, а значит, ещё больше кода. Проклятье, это было как раз то, что мы хотели предотвратить.
Типизация для nullsafe оператора
Вывод из второго наблюдения заключается в том, что последний метод объектного выражения не должен возвращать null
. Таким образом, неудачное объектное выражение может показать себя во всей красе, используя тип null
.
Кроме того, этот метод не должен возвращать void
: хотя этот тип отличается от null
, сбор возвращаемого значения void-функции дает... снова null
. Поэтому этого типа также следует избегать.
<?php
class What {
function foo() : int {}
function goo() : void {}
}
function getInstance($what) : ?What {}
// Возвращает значение int или NULL в случае ошибки
$result = getInstance('what')?->foo();
// Всегда возвращает null, из-за void
$result = getInstance('what')?->goo();
// Возвращает int в случае успеха и null в противном случае
// последнее -> надеюсь, будет работать всегда
$result = getInstance('what')?->foo1()->foo2()->property->goo();
$result = getInstance('what')?->foo1()->foo2()->property->property2;
?>
Наконец, обратите внимание, что только последний вызываемый элемент объектного выражения должен иметь не-null тип. В приведённом выше примере это метод goo
. Это может быть и типизированное свойство.
Все остальные методы и свойства между ними должны быть успешными, и их тип не имеет значения. В приведённом выше примере они должны всегда возвращать правильный тип, чтобы цепочка продолжалась. Больше ?->
также может быть допустимым.
Nullsafe оператор на практике со статическим анализом
?->
— это приятное синтаксическое усовершенствование по сравнению с ->
, поскольку оно делает проверку необязательной. Для любого достаточно дисциплинированного кода она работает просто отлично и предотвращает ошибки блокировки, одновременно уменьшая объем кода.
Тем не менее его следует контролировать на наличие двух дополнительных паттернов:
- результирующее значение должно сравниваться с
null
, для обеспечения контроля над ошибками - последний вызываемый метод или свойство не может быть типа
nullable
, чтобы не спутать одну ошибку с другой.
Основным преимуществом ?->
является переключение загрузки проверки с "до вызова" на "после вызова". Это даёт возможное преимущество, позволяющее полностью сохранить проверку, просто игнорируя её.
В исходном коде есть много подобных ситуаций: неинициализированные переменные, вызов int-типизированного аргумента с некоторыми строками, или json_decode()
без try/catch
: в подавляющем большинстве случаев всё работает, пока не появляется ошибка. А с помощью ?->
эта ошибка скрывается.