08.12.2022

13 неожиданностей в PHP, о которых знают не все

Казалось бы все знаешь, пишешь себе функции, используешь операторы и получаются у тебя крутые, быстрые и понятные конструкции кода, классы например. Но в какой-то момент внезапно нарываешься на ситуацию, когда код работает так как нужно ему, а не как ожидаешь ты и все это как-будто вопреки здравому смыслу. Но код никогда не ошибается, а ошибается наша логика…

Ниже поговорим про интересные и неожиданные случаи в PHP.

explode() в 2 раза быстрее unserialize()

Всегда лучше хранить числа через запятую, чем их же сериализовать.

$str = 'a:7:{i:0;i:654;i:1;i:654;i:2;i:654;i:3;i:654;i:4;i:654;i:5;i:654;i:6;i:654;}';
$str2 = '654,654,654,654,654,654,654';
for( $i=1; $i<500000; $i++ ){ unserialize($str); //> 0.43295 сек. explode(',', $str2); //> 0.22295 сек.
}
isset() в 2 раза быстрее in_array()

Скорости очень быстрые, но если обрабатываются большие массивы, то есть смысл заюзать array_flip() и искать значение через isset():

$arr = array( 5, 6, 7, 8, 9, 10, 11, 12, 13 );
$arr2 = array_flip( $arr ); // [5] => 0 [6] => 1 [7] => 2 [8] => 3 [9] => 4 [10] => 5 [11] => 6 [12] => 7 [13] => 8 for( $i = 1; $i < 500000; $i++ ){ in_array( 5, $arr ); //> 0.03150 сек. isset( $arr2[5] ); //> 0.01552 сек.
}
Точное сравнение

PHP язык без строгой типизации и потому иногда могут возникать неожиданные результаты при сравнении (проверке) разных значений…

if( 0 == 'строка' ) echo 'Неужели?';
// Условие сработает и мы увидим: 'Неужели?' // другими словами:
var_dump( 0 == 'строка' ); //> bool(true) // но
var_dump( '0' == 'строка' ); //> bool(false)

Происходит так очевидно, потому что ‘строка’ превращается в ноль: intval( ‘строка’ ) = 0, а 0 == 0 это true, разумеется…

Так например можно пропустить переменную запроса:

// $_GET['foo'] может быть любой строкой и проверка всегда будет срабатывать...
if( $_GET['foo'] == 0 ){ echo $_GET['foo'];
} // поэтому по возможности ставьте проверку строго по типу
if( $_GET['foo'] === 0 ){ echo $_GET['foo'];
}

Все следующие значения одинаковы, при сравнении через == (не строгий оператор сравнения):

0 == false == "" == "0" == null == array()

Ну и так:

1 == 'нечто' == true
true == array(111)

in_array() нас обманывает

Вы мастер массивов в PHP. Не стесняйтесь этого. Вы уже знаете все о создании, редактировании и удалении массивов. Тем не менее, следующий пример может вас удивить.

Часто при работаете с массивами приходится в них что-либо искать с помощью in_array().

$array = array( false, true, 1 ); if( in_array( 'строка', $array ) ){ echo 'Неужто нашлось';
}

Как думаете выведет этот пример надпись «Неужто нашлось»? После такого вопроса, вы наверняка решили что условие сработает, но при написании кода, скорее всего было бы наоборот — и вы бы решили что условие не сработает На самом деле, это условие сработает и код выведет надпись «Неужто нашлось».

Так происходит, потому что PHP язык бестиповой и in_array() в данном случае сравнивает значения, но не учитывает тип, т.е. использует оператор ==, а не ===. А ‘строка’ == true даст нам true. Вот и получается что in_array() лжёт!

Решение

Чтобы избежать такого «обмана», нужно указать true в третий параметр в in_array(), так все сравнения будут проходить с учетом типа значения.

$array = array( false, true, 1 ); if( in_array( 'строка', $array, true ) ) echo 'Неужто нашлось';
else echo 'Не найдено'; // сработает этот вариант условия

Разница между PHP операторами OR и ||, AND и &&

PHP операторы OR, AND и ||, && соответственно, отличаются приоритетами выполнения. У последних он выше, поэтому они будут выполняться раньше.

Если сравнивать с оператором присваивания: =, то OR/AND будут выполняться ПОСЛЕ оператора присваивания, в то время как у || и && будут выполняться ДО оператора присваивания, из за более высокого приоритета. Рассмотрим эту разницу на примере:

OR и ||
$true = true; // присваиваем
$false = false; // присваиваем $var = $false OR $true;
// $var будет равен false, потому что присваивание сработает раньше чем сравнение OR
// действует как: ( ($var = $false) or $true ) $var2 = $false || $true;
// $var2 = true, так как первым произошло сравнение, а уже потом присваивание var_dump( $var, $var2 ); // bool(false), bool(true)
AND и &&
// "&&" имеет больший приоритет, чем "and" $g = true && false;
// Результат выражения (true && false) присваивается переменной $g
// Действует как: ($g = (true && false)) $h = true and false;
// Константа true присваивается $h, а затем значение false игнорируется
// Действует как: (($h = true) and false) var_dump( $g, $h ); //> bool(false), bool(true)

Полезная ссылка по этой теме: Приоритет оператора

Шунтирующие операторы (короткая запись)

При сравнении типа AND (&&), если первое условие вернет false/0/»/array(), то нет смысла проверять следующие условия, потому что всё условие выполнится только если сразу все вложенные условия вернут что-либо отличное от empty (не false)…

При сравнении типа OR (||), если хоть одно условие вернет true или что-то отличное от empty, то нет смысла проверять следующие вложенные условия, потому что все условие выполняется когда хоть одно под-условие возвращает не false.

// foo() никогда не буде вызвана
// так как эти операторы являются шунтирующими (short-circuit) $a = false && foo();
$b = ( false and foo() );
$c = true || foo();
$d = ( true or foo() );

count() не всегда дает ожидаемый результат
var_dump( count(false) ); //> int(1)
var_dump( count(0) ); //> int(1)
var_dump( count('') ); //> int(1)
var_dump( count(array()) ); //> int(0) // или sizeof
var_dump( sizeof(false) ); //> int(1)
var_dump( sizeof(0) ); //> int(1)
var_dump( sizeof('') ); //> int(1)
var_dump( sizeof(array()) ); //> int(0)
isset() и null

Все мы привыкли проверять наличие ключа в массиве через isset(). Однако если элемент в массиве есть, но его значение null, то isset() вернет false, как если бы элемента в массиве вообще не было.

Наличие элемента со значением null можно проверить функцией array_key_exists().

$array = array('first' => null, 'second' => 4); isset( $array['first'] ); //> false array_key_exists( 'first', $array ); //> true
Странное поведение в PHP при передаче значения foreach по ссылке
$a = array('a', 'b', 'c'); foreach( $a as & $v ){ }
foreach( $a as $v ){ } print_r( $a ); /*
Array
( [0] => a [1] => b [2] => b
)
*/

Мы дважды проводим итерацию по массиву, ничего не делая. Так что в результате никаких изменений не должно быть. Правильно? — Неправильно!

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

foreach( $a as & $v ){}
unset($v);

Почему так происходит отлично объясняется тут.

empty() и объекты

Проверка empty() на объектах может вести себя странно. Допустим у нас есть некий объект $obj и мы проверяем пусто ли свойство var, и получаем такое:

if( empty( $obj->var ) ){ // условие сработает
} if( ! $obj->var ){ // условие не сработает
}

Парадокс! Как такое может быть? empty() говорит что свойство пустое, а ! говорит что в нем что-то есть. Как одно и тоже свойство может быть пустым и не пустым одновременно? Квантовая суперпозиция господа…

Однако если разобраться, то нет тут ничего удивительного и все логично!

Дело в том, что конструкция empty() обращается к встроенному методу объекта __isset(), а прямой запрос свойства ($obj->var) обратиться к __get().

Т.е. получается empty() и ! запрашивают разные методы, если свойство не установлено:

class FOO { function __get( $name ){ if( $name == 'bar' ) return true; } } $obj = new FOO; var_dump( empty($obj->bar) ); //> bool(true) - переменной нет var_dump( ! $obj->bar ); //> bool(false) - переменная есть

А теперь, зададим значение свойства bar в __isset() и empty() его получит:

class FOO { function __isset( $name ){ if( $name == 'bar' ) return true; } function __get( $name ){ if( $name == 'bar' ) return true; } } $obj = new FOO; var_dump( empty($obj->bar) ); //> bool(false) - переменная есть var_dump( ! $obj->bar ); //> bool(false) - переменная есть

Увеличитель числа ++

Имеет большое значение в каком положении использовать ++ (инкремент, увеличитель).

++$i — увеличивает $i на 1, сразу — при текущем вызове $i.
$i++ — увеличит $i на 1, при следующем вызове $i.

$i = 0; echo $i++; //> 0 - число увеличится при следующем вызове
echo $i; //> 1 - увеличилось
echo ++$i; //> 2 - число увеличивается сразу // сейчас $i = 2 // увеличивать можно внутри условий, индексов массивов - где угодно
if( $i++ == 2 ) echo $i; //> 3
$array[ ++$i ]; //> просим элемент массива с индексом 4 // однако нужно учитывать положение множителя - до или после переменной
// в обоих случаях проверяемое число будет разное...
// сейчас $i = 4
$array = array( 5 => 'foo' );
$array[ $i++ ]; //> ошибка - индекса нет, потому что мы просим 4

— уменьшитель (декремент) работает точно также…

Повторим еще раз:

Пример Название Действие
++$a инкремент до Увеличивает $a на 1, затем возвращает значение $a.
$a++ инкремент после Возвращает значение $a, затем увеличивает $a на 1.
--$a декремент до Уменьшает $a на 1, затем возвращает значение $a.
$a-- декремент после Возвращает значение $a, затем уменьшает $a на 1.

Увеличение строки ++

С числами все довольно просто, но что будет если инкрементить строки?

Что выведет данный код?

$a = 'fact_2';
echo ++$a; //> fact_3 $a = '2nd_fact';
echo ++$a; //> 2nd_facu $a = 'a_fact';
echo ++$a; //> a_facu $a = 'a_fact?';
echo ++$a; //> a_fact? $a = 'Привет';
echo ++$a; //> Привет

При инкременте строки, PHP увеличивает последний символ на символ следующий по алфавиту. И если в конце 2, то следующий символ будет 3. После t следует u. Однако эта операция не имеет никакого смысла в случае, когда строка заканчивается на не буквенно-численный символ.

Этот момент хорошо описан в официальной документации по операциям инкремента/декремента, однако многие не читали этот материал, потому что не ожидали встретить там ничего особенного.

Неточности с плавающей точкой

Посчитайте эту арифметику и скажите результат:

echo intval( (0.1 + 0.7) * 10 );

Сколько получилось, 8? А у компьютера 7!

Так происходит, потому что компьютеры не умеют хорошо работать с неточными числами — это как выясняется большая и старая проблема, есть даже статья на эту тему: «Что каждый компьютерщик должен знать об операциях с плавающей точкой».

Что получается в итоге и где комп ошибается? Суть кроется тут:

0.1 + 0.7 = 0.79999999999 0.79999999999 * 10 = 7.9999999999 intval( 7.9 ) = 7 а не 8. Когда значение приводится к int, PHP обрезает дробную часть.

Однако, если посчитать так, то увидим 0.8, а не 0.79999999999. Хотя этот результат является лишь округлением:

echo 0.1 + 0.7; //> 0.8

Какой вывод можно сделать из этого примера? — Будьте очень осторожны, когда дело касается дробных чисел (чисел с плавающей точкой) и никогда им не доверяйте.

Использовал при написании:

Статьи до этого: PHP

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *