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