Подводные камни PHP: работа с типами данных

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

Итак, предлагаю Вашему вниманию обзор тонкостей и корявостей языка, которые меня сразу же ошарашили при просмотре первого же раздела мануала про типы данных. Как оказалось, даже работа с типами данных в PHP таит в себе много опасностей. Данные подводные камни стоит рассматривать как «корявости», если Вы еще выбираете язык для своего проекта, или как «тонкости», о которых лучше знать и помнить, если у Вас уже нет выбора (как у меня).

Работа с типами данных в PHP

Boolean

1. Когда Вы конвертируете что-то в тип boolean, значения типа пустой строки или пустого массива, а также число 0 (будь то 0 или 0.00) рассматривается как FALSE.

Но при этом совершенно неясно, чем руководствовался автор языка, когда решил, что строка «0» тоже должна интерпретироваться как FALSE. При этом «0.00», конечно же, TRUE. *facepalm*

2. Комментарии к статьям в официальном мануале полны интересных советов и замечаний. Вот некоторые из них, касающиеся типа boolean.

При выводе булевых переменных PHP делает нечто странное. Рассмотрим код:

$var1 = TRUE;
$var2 = FALSE;

echo $var1; // Отображает число 1

echo $var2;//Ничего не выводит

echo (int)$var2; //Это выведет 0.

Прекрасно, правда?

3. PHP никогда не предупредит Вас о переводе типа данных из булевого в числовой, поэтому следует быть очень аккуратным. А порой это порождает дикие несуразицы.

var_dump(0 == 1); // false
var_dump(0 == (bool)'all'); // false
var_dump(0 == 'all'); // TRUE, осторожно!
var_dump(0 === 'all'); // false

«var_dump(0 == ‘all’)» — что здесь произошло? Так как мы попытались сравнить число и строку, то PHP попытается перевести строку в число. Если строка не переводима в число, то PHP не будет выдавать никаких ошибок или предупреждений. Он просто преобразует такую строку в ноль.

Чтобы избежать такого поведения, надо приводить число к строке:

var_dump((string)0 == 'all'); // false

4. Из предыдущего пункта следует, что любая строка не содержащая в начале чисел равняется нулю при переводе в число. В то же время, любая не пустая строка (кроме «0») переводится в TRUE. Таким образом:

0 == 'all';//TRUE
TRUE == 'all';//TRUE

При этом, конечно же 0 не равняется TRUE. Работа оператора == вообще не согласована, он не имеет свойства транзитивности.

5.  Рассмотрим еще один пример, который касается логического оператора:

$x=TRUE;
$y=FALSE;
$z=$y OR $x;

Чему равно $z? Конечно же FALSE!

Дело в том, что оператор OR имеет низкий приоритет, и присваивание $z=$y будет выполнено раньше. Т.е. вышеприведенный код эквивалентен такому:

($z=$y) OR $x

Что здесь происходит? Выполняется присваивание $z=$y, затем в результате присваивания возвращается TRUE (это не изъян PHP, в другом языке, вроде того же С++ при подстановке присваивания в условие всегда будет возвращаться TRUE). Так как уже было возвращено TRUE, то оператор OR далее ничего не делает, потому что остальные операнды уже не повлияют на результат.

Таким образом конечный результат совсем не тот, который ожидали. Что из этого следует? В силу своей низкой приоритетности, оператор OR скорее следует использовать как оператор альтернативного выполнения действий. Всем знакома данная конструкция:

doSomething() OR die()

В данном случае, если при исполнении функции doSomething произойдет ошибка, или в результате работы функции будет возвращено FALSE (что с точки зрения оператора OR одно и то же), будет выполнена функция die, так как проверке подлежат все операнды оператора до первого TRUE.

Отсюда мораль: если нужно просто проверить булево условие — используйте оператор || вместо OR, у него более высокий приоритет и работает он без сюрпризов.

Integer

1. До PHP 7, если в восьмеричном числе встречаются неразрешенные цифры (например 8 и 9), остальная часть числа начиная с этих цифр игнорируется! Конечно же, без каких-либо предупреждений. С PHP 7 интерпретатор выдает ошибку парсинга.

2. Если вы попытаетесь записать в переменную с типом integer число большее, чем верхняя числовая граница для этого типа, то это переменная автоматически приводится к типу float. Не такой уж и страшный косяк, но следует иметь это ввиду учитывая особенности типа float (например, сравнение переменных типа float и т.п., об этом подробнее написано ниже).

3. Если float выходит за границы integer (обычно +/- 2.15e+9 = 2^31 на 32-битных платформах и +/-9.22e+18 = 2^63 на 64-битных платформах, отличных от Windows), результат не определен, так как float не имеет достаточной точности, чтобы дать точный целочисленный результат. Когда это произойдет не будет выдано никакое предупреждение!

Но стоит отметить: начиная с PHP 7.0.0, вместо того, чтобы быть неопределенным и зависящим от платформы результатом, NaN и Infinity всегда будут равны нулю при приведении к целому числу.

4. Начальный ноль в числовом литерале означает: «это восьмеричное число». Но ведущий ноль в строке не делает число, полученное при приведении данной строки к типу integer восьмеричным. Таким образом:

$x = 0123;          // 83
$y = "0123" + 0     // 123

5. Приведение строки к типу integer работает только если строка начинается с цифр, и работает до первого не числового символа (вместо того, чтобы выдать предупреждение или что-то в этом роде).

(int) "5txt" //5
(int) "before5txt" //0
(int) "53txt" //53
(int) "53txt534text" //53

6. На 64-битных платформах максимальное целочисленное значение равно 0x7fffffffffffffff (9 223 372 036 854 775 807). И так сойдет!

Float

1.Хотелось бы отметить «особенность» поддержки PHP переменных с плавающей точкой, которая нигде не проясняется и сводит людей с ума.

Этот тест (где var_dump говорит, что $a=0.1 и $b=0.1)

if($a>=$b)
    echo "ok";

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

Чтобы исправить положение, Вам придется делать так:

if(round ($a,3)>=round ($b,3))
 echo "ok";

Этот код работает. Очевидно, даже если var_dump говорит, что переменные идентичны, и они должны быть идентичными, они таковыми не являются. Там присутствует какая-то скрытая точность. Возможно, это все же следовало добавить в документацию?

2. В PHP у строк есть одно замечательное свойство — результат приведения float к строке зависит от текущей локали. Например, если нижеприведенный код запустит человек из Штатов, то все будет ок:

$x = 0.23;
$js = "var foo = doBar($x);";
print $js;

Данный код выдаст строку «var foo = doBar(0.23);«.
Но если данный код запустит человек из Германии или Польши, где разделителем чисел с плавающей запятой является, собственно, запятая, то результат будет несколько иным: «var foo = doBar(0,23);«. Согласитесь, вызов функции с одним или двумя аргументами — это уже не одно и то же.

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

setlocale(LC_ALL, "pl_PL");

Если пойти еще дальше, и не просто перевести число float в строку, а перевести в строку и назад в float, то окажется, что все цифры после запятой будут отброшены! Вот пример (допустим у нас включена локаль pl_PL):

$a = 5/2;
echo (float)(string)$a;//выведет "2"

Вы можете возмутиться, что, мол, в реальной системе такой говнокод не встретишь и вообще, зачем так приводить типы? Но Вы не забывайте, что программы бывают сложными, и не обязательно переменная типа float у Вас будет приводиться в string и обратно прям на той же строке кода. Небольшое упущение — и, упс! У вас пропала дробная часть числа и Вы дерете волосы из головы, пытаясь найти в какой момент она исчезла.

Отсюда мораль: следите за локалями, и если хотите быть уверенными, что строка с числом будет отформатирована корректно (с точкой в качестве разделителя), используйте функцию number_format.

String

1. Булевое TRUE приводится к строке «1», тем временем как FALSE — к пустой строке. Как поясняют в мануале сами разработчики PHP, это позволяет производить конверсии между булевыми и строковыми переменными в обоих направлениях.

Остается только гадать, почему FALSE не приводится к строке «0», ведь как раз для такой строки и было придумано исключение. Такое впечатление, что после того как сделали интерпретацию строки «0» в качестве FALSE про это просто забыли.

2. Если Вы используете ассоциативный ключ массива внутри строки с двойными кавычками (double-quoted string) PHP выдает ошибку T_ENCAPSED_AND_WHITESPACE.

Рассмотрим код:

$fruit=array(
   'a'=>'apple',
   'b'=>'banana'
);

print "This is a $fruit['a']";//ошибка T_ENCAPSED_AND_WHITESPACE

Решение проблемы, возникшей на пустом месте:

print "This is a $fruit[a]";    //да, так можно
print "This is a ${fruit['a']}";//Complex Syntax для строк с переменными
print "This is a {$fruit['a']}";//вариация Complex Syntax

3. Используя специальный синтаксис внутри строк с двойными кавычками можно использовать переменные, поля класса и даже результаты вызова его методов. Однако, статические методы и поля класса, а также константы внутри класса Вы использовать уже не можете. Потому что парсер ищет символы ‘$’. А их нет. Смотрите:

class Test {
    const ONE = 1;
}
echo "foo {Test::ONE} bar"; //выведет foo {Test::one} bar

Вообще, какого черта для использования статических полей, методов и констант надо было придумывать какой-то особый синтаксис? Из-за этого единственный выход — разбивать строку не полагаясь на «чудо-парсинг».

Вместо заключения

В данной статье были рассмотрены ловушки при работе с простейшими типами данных в PHP. Следует понимать, что PHP изначально разрабатывался не для программистов на заре возникновения веба, отсюда такое количество огрех и неточностей при проектировании языка. А также принципы, вроде «пусть работает хоть как-то, лишь бы не падало и выдавало ошибки».

В следующей статье будут освещены нюансы работы с массивами и объектами (в качестве типов данных).

Похожие записи

Оставить комментарий