PHP Foreach Pass по ссылке: Дублирование последнего элемента? (Ошибка?)

У меня было очень странное поведение с простым php script, который я писал. Я уменьшил его до минимума, необходимого для воссоздания ошибки:

<?php

$arr = array("foo",
             "bar",
             "baz");

foreach ($arr as &$item) { /* do nothing by reference */ }
print_r($arr);

foreach ($arr as $item) { /* do nothing by value */ }
print_r($arr); // $arr has changed....why?

?>

Выводится:

Array
(
    [0] => foo
    [1] => bar
    [2] => baz
)
Array
(
    [0] => foo
    [1] => bar
    [2] => bar
)

Является ли это ошибкой или каким-то действительно странным поведением, которое должно произойти?

Ответ 1

После первого цикла foreach $item по-прежнему является ссылкой на некоторое значение, которое также используется $arr[2]. Поэтому каждый вызов foreach во втором цикле, который не вызывает по ссылке, заменяет это значение и, следовательно, $arr[2], с новым значением.

Итак, цикл 1, значение и $arr[2] становятся $arr[0], что является "foo".
Петля 2, значение и $arr[2] становятся $arr[1], что является "баром".
Петля 3, значение и $arr[2] становятся $arr[2], что является "баром" (из-за цикла 2).

Значение "baz" фактически теряется при первом вызове второго цикла foreach.

Отладка выходных данных

Для каждой итерации цикла мы будем эхо-значения $item, а также рекурсивно распечатать массив $arr.

Когда выполняется первый цикл, мы видим этот вывод:

foo
Array ( [0] => foo [1] => bar [2] => baz )

bar
Array ( [0] => foo [1] => bar [2] => baz )

baz
Array ( [0] => foo [1] => bar [2] => baz )

В конце цикла $item все еще указывает на то же место, что и $arr[2].

Когда выполняется второй цикл, мы видим этот вывод:

foo
Array ( [0] => foo [1] => bar [2] => foo )

bar
Array ( [0] => foo [1] => bar [2] => bar )

bar
Array ( [0] => foo [1] => bar [2] => bar )

Вы заметите, как каждый раз массив помещал новое значение в $item, он также обновлял $arr[3] с тем же значением, поскольку оба они все еще указывают на одно и то же местоположение. Когда цикл попадает в третье значение массива, он будет содержать значение bar, потому что он был просто установлен предыдущей итерацией этого цикла.

Это ошибка?

Нет. Это поведение ссылочного элемента, а не ошибка. Это было бы похоже на запуск чего-то вроде:

for ($i = 0; $i < count($arr); $i++) { $item = $arr[$i]; }

Цикл foreach не является особым в природе, в котором он может игнорировать ссылочные позиции. Он просто устанавливает эту переменную в новое значение каждый раз, как если бы вы не были за пределами цикла.

Ответ 2

$item является ссылкой на $arr[2] и перезаписывается вторым циклом foreach, как указывал animuson.

foreach ($arr as &$item) { /* do nothing by reference */ }
print_r($arr);

unset($item); // This will fix the issue.

foreach ($arr as $item) { /* do nothing by value */ }
print_r($arr); // $arr has changed....why?

Ответ 3

Хотя это, возможно, официально не ошибка, по-моему, это так. Я думаю, что проблема здесь в том, что мы ожидаем, что $item выйдет за пределы области действия, когда цикл будет завершен, как и во многих других языках программирования. Однако, похоже, это не так...

Этот код...

$arr = array('one', 'two', 'three');
foreach($arr as $item){
    echo "$item\n";
}    
echo $item;

Дает выход...

one
two
three
three

Как уже говорили другие люди, вы переписываете ссылочную переменную в $arr[2] вторым циклом, но это происходит только потому, что $item никогда не выходил за рамки. Что вы, ребята, думаете... ошибка?

Ответ 5

Правильное поведение PHP может быть ошибкой NOTICE в моем оппионе. Если ссылочная переменная, созданная в цикле foreach, используется вне цикла, она должна вызывать уведомление. Очень легко пасть за это поведение, очень сложно определить его, когда это произошло. И ни один разработчик не собирается читать страницу документации foreach, это не поможет.

Вы должны unset() ссылку после цикла, чтобы избежать такой проблемы. unset() в ссылке просто удалит ссылку без ущерба для исходных данных.