Nullsafe оператор на практике

Источник: «Null safe operator in practice»
Null-safe оператор был добавлен в PHP 8.1: это новый объектный оператор, предотвращающий фатальную ошибку и последующую остановку выполнения при вызове метода или свойства со значением null. Он имеет большую привлекательность, поскольку называется "безопасным" и сокращает количество проверок перед вызовом метода. Однако на практике у оператора 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 оператор на практике со статическим анализом

?-> — это приятное синтаксическое усовершенствование по сравнению с ->, поскольку оно делает проверку необязательной. Для любого достаточно дисциплинированного кода она работает просто отлично и предотвращает ошибки блокировки, одновременно уменьшая объем кода.

Тем не менее его следует контролировать на наличие двух дополнительных паттернов:

Основным преимуществом ?-> является переключение загрузки проверки с "до вызова" на "после вызова". Это даёт возможное преимущество, позволяющее полностью сохранить проверку, просто игнорируя её.

В исходном коде есть много подобных ситуаций: неинициализированные переменные, вызов int-типизированного аргумента с некоторыми строками, или json_decode() без try/catch: в подавляющем большинстве случаев всё работает, пока не появляется ошибка. А с помощью ?-> эта ошибка скрывается.

Дополнительные материалы

Предыдущая Статья

6 лучших практик, советов и приёмов Tailwind CSS на 2024 год

Следующая Статья

Понимание хелпера fake() в Laravel