Что такое null и как с этим жить: сравнение с null в PHP, тонкости в приведении типов данных

В работе с любыми данными то и дело требуется как-то обозначить их отсутствие. Новички зачастую для этой цели используют значение false, которое, по сути, является не отсутствием данных, а определённым значением типа boolean. Для того, чтобы как-то помечать неопределённость или отсутствие данных, существует специальное значение null. Про false, null и прочее уже немало сказано и добавить что-то оригинальное крайне сложно. Но гораздо сложнее бывает найти баг, вызванный неочевидным поведением этих штуковин. Поэтому, вдохновлённый одним таким багом, самым элегантным в моём послужном списке, я решил написать маленькую шпаргалку на эту тему.


Перегружать пост интересными, но довольно-таки бесполезными определениями из Википедии я не стану. Вместо этого перейду сразу к делу и приведу цитату со страницы php.net, посвящённой null:

Специальное значение NULL представляет собой переменную без значения. NULL - это единственно возможное значение типа null. Переменная считается null, если:

  • ей была присвоена константа NULL.
  • ей еще не было присвоено никакого значения.
  • она была удалена с помощью unset().

До этого момента всё кажется простым, но чтобы так оставалось и дальше, при работе с null нужно придерживаться определённых правил.

Математические операции с null

Во всех математических операциях null ведёт себя аналогично int(0):


$a = null;
$a + 1; // int(1)
$a - 1; // int(-1)
$a * 1; // int(0)
1 / $a; // bool(false)
                    

Проверка переменной на null

Чтобы точно узнать, что переменная содержит null (то есть, ничего не содержит), нужно использовать либо специальную функцию is_null() либо тождественное сравнение ===, а любые другие способы не подойдут:


// Правильная проверка переменной на null
$a = null;
is_null($a); // bool(true)
$a === null; // bool(true)
                    

Проверка null if-выражениях, а так же в функциях empty() и isset()

Тут переменные с null ведут себя абсолютно предсказуемо, как и любые другие ложные значения (которые в if-выражениях приводятся к false). Но нужно помнить, что это не гарантирует нам, что в переменной находится именно null:


// В if-выражениях null приводится к false, как и все другие ложные значения
if($a) {
    echo 'Sure, $a is not null!';
}
else {
    echo 'Mabye $a is null.'
}

// Проверка на empty() вернёт true, но не гарантирует, что проверяемая переменная - именно null
$a = null;  empty($a); // bool(true)
$a = [];    empty($a); // bool(true)
$a = '';    empty($a); // bool(true)
$a = 0;     empty($a); // bool(true)
$a = false; empty($a); // bool(true)

// Проверка isset() вернёт false не только для null, но для не определённой переменной
$a = null;
isset($a);         // bool(false)
isset($undefined); // bool(false)
                    

Сравнение двух null

В PHP как гибкое, так и тождественное сравнение двух null всегда возвращает false, в отличие от многих других платформ, где сравнение двух неизвестностей возвращает так же неизвестность (то есть, null).


// PHP уверен, что две неизвестности равны, и даже тождественно равны
$a = null; $b = null;
$a == $b;  // bool(true)
$a === $b; // bool(true)
                    

Поведение null при нетождественном сравнении с приведением типов данных

PHP разрешает сравнивать null-ы не только между собой, но и с другими типами данных. При этом не стоит забывать, что гибкое сравнение в php не является транзитивным, то есть, если два значения равны третьему по отдельности, не гарантирует их равенство между собой. Согласно таблицам приведения типов, гибкое сравнение null при помощи оператора == с разными значениями возвращает разные результаты:


// Для всех ложных значений гибкое сравнение с null возвращает true, кроме строки '0'
$a = [];    $a == null; // bool(true)
$a = '';    $a == null; // bool(true)
$a = 0;     $a == null; // bool(true)
$a = false; $a == null; // bool(true)

// А при сравнении null со строкой '0' мы получим false, хотя null == false и '0' == false
$a = '0'; $a == null; // bool(false)

// Для всех неложных значений такое сравнение так же вернёт false
$a = 1;   $a == null;  // bool(false)
$a = '1'; $a == null;  // bool(false)
                    

Сравнение с null с помощью >, <, >=, <=, сравнение с отрицательными числами и другие заковырки

Не мне судить, насколько это является очевидным, но при сравнении с числами null всегда оказывается меньше них. Смотрите сами:


// Не смотря на то, что null == 0, он оказывается меньше любого числа
null <= -1    // bool(true)
null <= 0     // bool(true)
null <= 1     // bool(true)
!(null >= -1) // bool(true)
null >= 0     // bool(true)
!(null >= 1)  // bool(true)
                    

На вид ничего особенного, но из этого следует, что если в дело вмешивается null, может пройти бессмысленная, на первый взгляд, проверка:


// Из-за подобного поведения я словил однажды баг, из-за которого почти поверил в магию :)
$a = null;
if($a <= -1 && $a >= 0) {
    echo '$a <= -1 and $a >= 0 at the same time!';
}
                    

Разное поведение null в PHP, SQL и JavaScript

А вот это, на мой взгляд, самое скверное. Реализуя логику работы с ячейками баз данных, в которых разрешено значение null, на разных этапах продвижения данных от сервера к пользователю (например, MySQL -> PHP -> JavaScript) поведение null может меняться.

Исследование null в JavaScript (а вместе с ним и загадочного undefined) заслуживает отдельной статьи. Главное отличие состоит в том, что, не смотря на приведение типов, null в JavaScript ничему не равен, кроме самого null и этого самого undefined, хотя в if-выражениях и срабатывает аналогично false. А при сравнении с числами он выдаёт ещё более забавные результаты, чем в PHP.

NULL в MySQL, к примеру, действует гораздо более прямолинейно. Он просто при любых действиях с null (даже при сравнении двух null) возвращает null. С его точки зрения, при любых действиях с неизвестностью в результате получится какая-то другая неизвестность :)

Простое правило при работе null, которое помогает избегать проблем

Вообще-то, не стоило читать этот пост. Держать в голове все эти нюансы, которые разные платформы трактуют по-разному, - неблагодарное занятие. Для того, чтобы избежать проблем с null нужно запомнить всего лишь одно правило:

Старайтесь не выполнять с null никаких лишних действий: не нужно его ни с чем сравнивать, складывать, сортировать. Там, где чисто теоретически переменная с null может просочиться в какие-то операторы, лучшим решением будет сперва проверить её функцией is_null().