Не для кого из веб-разработчиков не секрет, что PHP является простым, гибким и не требовательным языком. Но при работе с этим языком можно столкнуться с неожиданными вещами. В этой статье я представлю «странные факты» и объясню, почему PHP дает такие результаты.
Неточности с плавающей точкой
Большинство из вас, наверное, знают, что числа с плавающей точкой не могут реально представить все действительные числа. Кроме того, некоторые операции между двумя вроде бы хорошо заданными числами могут привести к неожиданным ситуациям. Это потому, что точность, с которой компьютер хранит числа, имеет свои особенности. Данное явление сказывается не только на PHP, но и на всех языках программирования. Неточность в операциях с плавающей точкой доставляла немалую головную боль программистам начиная со дня основания дисциплины как таковой.
Об этом факте уже было не раз написано. Одна из наиболее важных статей — Что каждый компьютерщик должен знать об операциях с плавающей точкой. Если вы никогда не читали ее, я настоятельно рекомендую вам исправить эту ситуацию.
Давайте посмотрим на этот небольшой кусок кода:
<?php
echo (int) ((0.1 + 0.7) * 10);
Как вы думаете, каким будет результат? Если вы предполагаете, что результатом операции будет 8, то вы ошибетесь. На самом деле 7. Тем, кто имеет сертификат Zend, данный пример уже известен. К слову, вы можете найти данный пример в Руководстве по подготовке к сертификации Zend.
Теперь давайте посмотрим, почему же так происходит:
0.1 + 0.7 = 0.79999999999
Результат данной операции хранится в реальности как 0.79999999999, а не 0.8, как можно было бы подумать. Именно здесь и начинаются проблемы.
Вторая операция, которую мы выполняем:
0.79999999 * 10 = 7.999999999
Эта операция работает как надо, но проблемы остались.
Наконец, третья и последняя операция:
(int) 7.9999999 = 7
Данное выражение использует явное приведение типов. Когда значение приводится к int, PHP обрезает дробную часть и в итоге возвращает 7.
- Если вы приведете данное выражение к типу float, а не int, или вообще не будете делать приведения типов, то вы получите число 8, как и ожидали
- Именно из-за того, что существует математический парадокс, что 0.999… равно 1, мы и получили эту ошибку.
В заключение данного пункта, я хотел бы процитировать выдержку из Руководства по подготовке к сертификации Zend:
Всякий раз, когда точность играет решающую роль в ваших приложениях, вы должны рассматривать вопрос об использовании математического расширения PHP для работы с произвольной точностью — BC Math.
Как PHP «инкрементит» строки
Во время работы мы все время используем операции инкремента/декремента, подобные данным:
<?php
$a = 5;
$b = 6;
$a++;
++$b;
Каждый из нас с легкостью понимает, что тут происходит. Но попробуйте прикинуть, что выведет данный код:
<?php
$a = 1;
$b = 3;
echo $a++ + $b;
echo $a + ++$b;
echo ++$a + $b++;
Давайте посмотрим:
4 6 7
Не так уж и сложно, верно? Теперь давайте немного увеличим сложность. Вы когда-нибудь до этого пытались инкременить строки? Попробуйте предположить, что выведет данный код:
<?php
$a = 'fact_2';
echo ++$a;
$a = '2nd_fact';
echo ++$a;
$a = 'a_fact';
echo ++$a;
$a = 'a_fact?';
echo ++$a;
Это задание уже посложнее. Давайте посмотрим, что мы получили:
fact_3 2nd_facu a_facu a_fact?
Удивлены? Делая инкремент строки, которая заканчивается на цифру, мы фактически будем увеличивать символ (на следующий символ по алфавиту, т.е. после t следует u). Независимо от того, начинается строка с цифры или нет, последний символ будет изменен. Однако эта операция не имеет никакого смысла в случае, когда строка заканчивается на не буквенно-численный символ.
Этот момент хорошо описан в официальной документации по операциям инкремента/декремента, однако многие не читали этот материал, потому что не ожидали встретить там ничего особенного. Хочу признаться, что до недавнего времени я думал точно так же. Собственно выдержка из документации:
PHP следует соглашениям Perl (в отличие от С) касательно выполнения арифметических операций с символьными переменными. Например, в PHP и Perl $a = ‘Z’; $a++; присвоит $a значение ‘AA’, в то время как в C a = ‘Z’; a++; присвоит a значение ’[‘ (ASCII значение ‘Z’ равно 90, а ASCII значение ’[‘ равно 91).
Следует учесть, что к символьным переменным можно применять операцию инкремента, в то время как операцию декремента применять нельзя, кроме того, поддерживаются только ASCII символы (a-z и A-Z). Попытка инкремента/декремента других символьных переменных не будет иметь никакого эффекта, исходная строка останется неизменной.
Тайна значений
Вы мастер массивов в PHP. Не стесняйтесь этого. Вы уже знаете все о создании, редактировании и удалении массивов. Тем не менее, следующий пример может удивить вас.
Очень часто при работаете с массивами вам приходится что-либо искать в них. В PHP есть специальная функция для этого in_array(). Давайте посмотрим ее в действии:
<?php
$array = array(
'isReady' => false,
'isPHP' => true,
'isStrange' => true
);
var_dump(in_array('phptime.ru', $array));
Что должно быть выведено?
true
Не правда ли, немного странно. У нас есть ассоциативный массив, в котором содержаться только буленовские значения, и когда мы выполняем поиск строки, получаем true. Действительно ли это волшебство? Давайте посмотрим другой пример:
<?php
$array = array(
'count' => 1,
'references' => 0,
'ghosts' => 1
);
var_dump(in_array('aurelio', $array));
И что мы получаем?
true
И снова in_array() вернула true. Как это возможно?
Только что вы использовали одну из любимых и одновременно ненавистных PHP-функций. Надо сказать, что PHP — не строго типизированный язык. Многие проблемы происходят именно из-за этого. На самом деле, все следующие значения, если их сравнивает PHP, идентичны при использовании одинарного оператора сравнения:
0 false "" "0" null array()
По умолчанию, in_array() использует «гибкое» сравненте, поэтому непустая («») и ненулевая строка («0») эквивалентны true, это же относится и ко всем ненулевым элементам (напр. 1). Следовательно, в нашем первом примере мы получили true, потому что’phpmaster.com’ == true, в то время как во втором примере ‘aurelio’ == 1.
Для решения этой проблемы вы должны использовать третий дополнительный параметр в функции in_array(), который позволяет строгое сравнивание элементов. Если мы теперь напишем:
<?php
$array = array(
'count' => 1,
'references' => 0,
'ghosts' => 1
);
var_dump(in_array('aurelio', $array, true));
мы наконец-таки получим значение false
Заключение
В этой статье вы видели странное и неожиданное поведение PHP-интерпретатора. Вот что вы могли извлечь из прочитанного:
- никогда не доверяйте числам с плавающей точкой;
- дважды проверьте тип данных перед их использованием;
- будьте в курсе проблем «гибкого» сравнения и «гибких» типов.
Если вы продвинутый программист, то, скорее всего, уже знали о существовании этих странностей, но повторение никогда не бывает бесполезным.