Равные или одинаковые. Как сравнить переменные?

== или ===? Сколько знаков равенства нужно поставить, чтобы это было правильно и чтобы ни у кого в code review не возникло с этим проблем? Почему это так сложно в PHP?

Жонглирование типами

В 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 . Да. Недостаточно, чтобы тип (класс) переменной и все ее свойства совпадали, она должна быть одним и тем же объектом. Сравнение == будет сравнивать тип объекта и сравнивать все его свойства (также с помощью ==).

Так как же следует сравнивать?

В примерах, которые я показал, видно, что использование сравнения == иногда может дать результат, отличный от ожидаемого. Как с этим бороться? Каждый раз, когда вы пишете == (два знака равенства), должна мигать красная лампочка. Мы всегда должны использовать оператор тождества === (три знака равенства), который также сравнивает тип переменной и ничего перед этим не приводит. Ну, почти всегда.

Единственный раз, когда мы можем использовать равенство == вместо оператора тождества ===, это когда мы делаем это намеренно, и у нас есть на это причины!


  1. https://www.php.net/manual/en/language.types.type-juggling.php

  2. https://www.php.net/manual/en/migration74.new-features.php

  3. https://www.php.net/manual/en/language.operators.comparison.php

  4. https://github.com/php/php-src/blob/PHP-7.4.2/Zend/zend_operators.c#L2022

  5. https://www.php.net/manual/en/language.types.float.php

  6. https://github.com/php/php-src/blob/PHP-7.4.2/Zend/zend_operators.c#L2908

  7. https://www.php.net/manual/en/language.oop5.object-comparison.php

Оцените статью
devanswers.ru
Добавить комментарий