== или ===? Сколько знаков равенства нужно поставить, чтобы это было правильно и чтобы ни у кого в code review не возникло с этим проблем? Почему это так сложно в PHP?
- Жонглирование типами
- Неравные равенства
- числовое значение — строка с числом и чем-то дополнительным
- числовое значение — строка, в начале которой стоит не число.
- числовое значение — строка с большим числом
- числовое значение — строка с числом и белыми символами
- string — строка
- объект — объект
- Так как же следует сравнивать?
Жонглирование типами
В PHP тип переменной определяется тем, как она была использована*. В зависимости от того, что мы присвоили переменной, она приобретает такой тип1. Что это значит?
(* Начиная с PHP 7 мы можем определять типы аргументов методов, с 7.4 можно определять типы свойств классов!2)
$foo = "1"; // $foo is string
$foo += 0; // $foo is integer
$foo *= 1.5; // $foo is float
В примере выше переменной была присвоена строка «1», поэтому переменная имеет строковый тип, затем к ней было добавлено целое число 0, поэтому ее тип изменился на целочисленный. После этого умножим целочисленную переменную на float 1.5. Теперь переменная имеет тип float.
Этот пример показывает, как автоматическое преобразование выполняется операторами, в данном случае сложением и умножением. Достаточно, чтобы одна из сторон действия имела тип float, тогда обе будут рассматриваться как float, и результат также будет иметь этот тип. Аналогичная ситуация с целыми числами, хотя float «сильнее», и в случае integer + float обе стороны будут рассматриваться как float.
Неравные равенства
Давайте перейдем к равенству, для начала заглянем в документацию php3 :
Equal: $a == $b, TRUE, если $a равно $b после жонглирования типами.
Звучит тривиально и выглядит естественно. Жонглирование типами приводит к тому, что не нужно много думать о том, что мы сравниваем, все делается автоматически… 😉 но так ли это?
Время для викторины, будут ли выполнены следующие условия?
0 == "eleven"
0 == "wtf-1.3e3"
1 == "nr1t"
10 == "10 - 10"
"-1300" == "-1.3e3"
9223372036854775807 == "9223372036854775811"
"1.00000000000000001" == "1.00000000000000002"
А теперь внимание, барабаны… в каждом из перечисленных случаев это будет верно. Что только что произошло? До сих пор весьма интуитивно понятный оператор сравнения теперь ведет себя довольно неестественным для глаза образом.
Как же на самом деле работает оператор сравнения? Чтобы выяснить это, лучше всего посмотреть, как он был реализован4. Однако это не простое чтение, потому что код там очень сложный. Вкратце, работа оператора == зависит от типа его аргументов. Определяются пары типов аргументов. Если данная пара поддерживается (известно, как их сравнивать), то возвращается результат сравнения, но если данная пара типов не поддерживается, то аргументы приводятся к другому типу (Type Juggling) и только потом сравниваются.
В большинстве случаев это работает предсказуемым образом, но есть пары типов, которые могут преподнести сюрприз.
числовое значение — строка с числом и чем-то дополнительным
Когда строка сравнивается с целым числом или числом float, то строка приводится к числовому значению, но это приведение иногда может быть неожиданным.
10 == "10 - 10"; // true
В этом случае значение строки приводится к целому числу, если строка начинается с числа, то все, что идет после этого числа, игнорируется. В данном случае «10 — 10» не будет результатом вычитания, поэтому не 0, а 10.
числовое значение — строка, в начале которой стоит не число.
0 == "Lorem ipsum dolor sit 0"; // true
Если строка не начинается с числа, то вместе с числовым значением она всегда будет приводиться к 0.
числовое значение — строка с большим числом
9223372036854775807 == "9223372036854775811"; // true
Целое число из этого сравнения является максимальным целым значением — PHP_INT_MAX (на 64-битных платформах), строка же содержит число чуть большее, всего в 4 ;). В этом случае целое число будет приведено к float, поэтому строка в конечном итоге также будет приведена к float. Такова природа float.
Поэтому никогда не доверяйте результатам с плавающей запятой до последней цифры и не сравнивайте числа с плавающей запятой напрямую на равенство.
Это предупреждение из документации PHP5 поэтому лучше не сравнивать значения с плавающей точкой напрямую, а использовать специальные функции:
числовое значение — строка с числом и белыми символами
1.0 == "nr1t"; // true
Белые символы игнорируются при приведении к числовому значению. Если вы посмотрите на пример выше, то найдете один «nr1t».
string — строка
При сравнении двух строковых значений было бы естественно, если бы это было верно, когда они одинаковые ;), но что значит «одинаковые».
"n1" == "r1"; // true
"1.0" == "t1"; // true
"1e0" == "1"; // true
"-0.5e-2" == "-0.005"; // true
"1.00000000000000001" == "1.00000000000000009"; // true
"10" == "0xA"; // true PHP < 7
// false PHP >= 7
При сравнении аргументов строкового типа в начале оба аргумента приводятся к числовому значению. Если это удается, то они сравниваются как числовые значения. Интересно, что PHP может проделать много трюков при поиске числа в строке6. Научная нотация или пробельные символы не являются проблемой, также как и шестнадцатеричная нотация до версии 7.0.
объект — объект
Если до сих пор все было слишком понятно и предсказуемо, то при сравнении объектов все работает немного иначе.
class Example {
public $property = 1;
}
$foo = new Example();
$bar = new Example();
$bar->property = 1.0;
$foo == $bar; // true
$foo === $bar; // false
Учитывая то, что я написал ранее, в приведенном примере все правильно. Переменные $foo и $bar имеют одинаковый тип (класс Example), их свойство ($property) в $foo равно 1 (integer), а в $bar равно 1.0 (float). Таким образом, результат сравнения == истинен. Поскольку значения совпадают, типы не обязательно должны совпадать (integer vs. float). Для === результат будет ложным, потому что типы свойств класса не совпадают?
$baz = new Example();
$qux = new Example();
$baz == $qux; // true
$baz === $qux; // false
Что здесь произошло? Обе переменные имеют одинаковый тип, их свойство имеет одинаковое значение и одинаковый тип, и все равно результат === — false.
При сравнении объектов тождество (===) возвращает истину только в том случае, если сравниваются экземпляры одного класса.7 . Да. Недостаточно, чтобы тип (класс) переменной и все ее свойства совпадали, она должна быть одним и тем же объектом. Сравнение == будет сравнивать тип объекта и сравнивать все его свойства (также с помощью ==).
Так как же следует сравнивать?
В примерах, которые я показал, видно, что использование сравнения == иногда может дать результат, отличный от ожидаемого. Как с этим бороться? Каждый раз, когда вы пишете == (два знака равенства), должна мигать красная лампочка. Мы всегда должны использовать оператор тождества === (три знака равенства), который также сравнивает тип переменной и ничего перед этим не приводит. Ну, почти всегда.
Единственный раз, когда мы можем использовать равенство == вместо оператора тождества ===, это когда мы делаем это намеренно, и у нас есть на это причины!
-
https://www.php.net/manual/en/language.types.type-juggling.php
-
https://www.php.net/manual/en/migration74.new-features.php
-
https://www.php.net/manual/en/language.operators.comparison.php
-
https://github.com/php/php-src/blob/PHP-7.4.2/Zend/zend_operators.c#L2022
-
https://www.php.net/manual/en/language.types.float.php
-
https://github.com/php/php-src/blob/PHP-7.4.2/Zend/zend_operators.c#L2908
-
https://www.php.net/manual/en/language.oop5.object-comparison.php