PHP
January 2012 23

Подводный камень в foreach($items as &$item)

Многие любят писать такие конструкции в том или ином виде, каждый сталкивался:
foreach ($items as &$item) {
    $item += 2;
}

Но не многие подозревают о том, какая опасность тут скрывается.
Рассмотрим пример.

Вася Пупкин взял массив, прошелся по нему, увеличив на два все элементы:
$items = array(
    'a' => 10,
    'b' => 20,
    'c' => 30,
);

foreach ($items as &$item) {
    $item += 2;
}

print_r($items);

Посмотрел дамп, увидел что задача решена, и ушел довольный:
Array
(
    [a] => 12
    [b] => 22
    [c] => 32
)

Спустя некоторое время, Петрович решил дополнить этот участок кода другим перебором, дописав ниже:
$newitems = array(
    'a' => 10,
    'b' => 20,
    'c' => 30,
);

foreach ($newitems as $key=>$item) {
    $newitems[$key] += 5;
}

print_r($newitems);

Посмотрел, что его задача тоже решена, и с чувством выполненного долга закрыл файл:
Array
(
    [a] => 15
    [b] => 25
    [c] => 35
)

Спустя какое-то время, стали вылезать необъяснимые баги. Почему?
Сделаем в конце кода var_dump($items):
array(3) {
  ["a"]=>
  int(12)
  ["b"]=>
  int(22)
  ["c"]=>
  &int(30)
}

30! Вася Пупкин клянётся, что проверял. Почему было 32, а после кода Петровича 30?

Причина кроется в амперсанде. Он сообщает, что на отмеченные данные ссылается кто-то ещё. Уходя, Вася не подтёр за собой временную переменную, которую использовал для перебора ($item). Переменная использовалась с разрешением на изменение источника ("&"), которое также называют «присваиванием по ссылке». Он был уверен, что переменная будет использоваться только внутри цикла. Петрович, используя переменную с таким же именем, в ходе своего перебора, менял её значение, и каждый раз менялось то место, где эта переменная хранилась. А хранилась она там же, где последний элемент массива Пупкина.

Конечно, в случай в статье утрирован. На практике такие связи могут быть очень сложными, особенно если проект недорогой, и в нём участвуют недостаточно опытные и разрозненные веб-разработчики.

Как можно с этим оброться?
  • Уничтожать временные переменные после использования, особенно если они имеют какие-то связи с используемыми данными:
    foreach ($items as &$item) $item += 2;
    unset($item);
    
  • Быть осторожнее с переменными, которые уже кем-то использовались.
  • Инкапсулировать свои действия в отдельные функции, методы или пространства имён.
  • Использовать var_dump, вместо print_r, и обращать внимание на амперсанд. Чтобы дампить в файл, а не в браузер, альтернативой print_r($var,true) будет такая конструкция:
    function dump() {
        ob_start();
        foreach(func_get_args() as $var) var_dump($var);
        return ob_get_clean();
    }
    

В заключение скажу, что баги, связанные со ссылками, могут быть не только в foreach. И все они когда-то обсуждались. Однако, этот случай, судя по моему опыту, так распространён на практике, что заслуживает отдельного внимания.
+70
63.2k 246
Comments 145