Не для кого из веб-разработчиков не секрет, что 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-интерпретатора. Вот что вы могли извлечь из прочитанного:

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

Если вы продвинутый программист, то, скорее всего, уже знали о существовании этих странностей, но повторение никогда не бывает бесполезным.


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

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