WordPress как на ладони
Наставник Трепачёв Д.П., phphtml.net wordpress jino

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 'Неужто нашлось';
}

Как думаете выведет этот пример надпись «Неужто нашлось»? Не знаю как вы подумали сейчас, но при написании кода, вы бы точно подумал, что все ок smile А ответ такой, - Да, выведет, хоть её там и нет.

Так происходит, потому что 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

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

-

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

13 неожиданностей в PHP, о которых знают не все 4 комментария
  • Денис Радченко

    Большинство "неожиданостей" получается от незнания языка или не чтения документации.

    Ответить24 дня назад #
  • Otshelnik-Fm179 cайт: across-ocean.otshelnik-fm.ru
    @

    В примере опечатка 0.79999999999 * 10 = 0.79999999999
    7.9999999999 должно быть

    За статью спасибо. Моменты интеерсные

    1
    Ответить24 дня назад #

Здравствуйте, !

Ваш комментарий