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


159

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

<?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
)

Это ошибка или какое-то странное поведение, которое должно произойти?


Сделайте это снова по значению, посмотрите, изменится ли в 3-й раз ...?
Шекрок

1
@Shackrock, он больше не меняется с повторяющимися циклами по значению.
регулярность

1
Интересно, что если вы измените второй цикл, чтобы использовать что-то отличное от $ item, он будет работать как положено.
Стив Кларидж

9
всегда сбрасывайте элемент в конце тела цикла: foreach($x AS &$y){ ... unset($y); }- это на самом деле на php.net (не знаю где), потому что это большая ошибка.
Руди

Ответы:


170

После первого цикла 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 не является особенным по своей природе, в котором он может игнорировать ссылочные элементы. Это просто установка этой переменной на новое значение каждый раз, как если бы вы были вне цикла.


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

2
Это поведение сложно понять и может привести к проблемам. Я держу это в качестве одного из моих любимых, чтобы показать моим студентам, почему они должны избегать (до тех пор, пока они могут) вещей "по ссылке".
Оливье Понс

1
Почему не $itemвыходит за рамки при выходе из цикла foreach? Это похоже на проблему закрытия?
jocull

6
@jocull: в PHP foreach, for, while и т. д. не создают свою собственную область видимости.
animuson

1
@jocull, PHP не имеет (блокирует) локальные переменные. Одна из причин, это меня раздражает.
Qtax

29

$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никогда не выходило за рамки. Что вы, ребята, думаете ... ошибка?


4
1) Не ошибка. Это уже упоминается в руководстве и отклонено в ряде отчетов об ошибках, как и предполагалось. 2) На самом деле не отвечает на вопрос ...
BoltClock

Он поймал меня не из-за проблемы с областью видимости, я ожидал, что $ item останется после первоначального foreach, но я не осознавал, что foreach ОБНОВЛЯЕТ переменную вместо ЗАМЕНЫ. например, аналогично запуску unset ($ item) перед вторым циклом. Обратите внимание, что unset не очищает значение (и, следовательно, последний элемент в массиве), он просто удаляет переменную.
Programster

К сожалению, PHP не создает новую область видимости для циклов или {}блоков в целом. Вот как работает язык
Фабиан Шменглер


0

Правильное поведение PHP может быть ошибкой УВЕДОМЛЕНИЯ по моему мнению. Если ссылочная переменная, созданная в цикле foreach, используется вне цикла, это должно вызвать уведомление. Очень легко поддаться такому поведению, очень трудно определить, когда это произошло. И ни один разработчик не собирается читать страницу документации по foreach, это не помощь.

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


0

это потому, что вы используете директиву ref (&). Последнее значение будет заменено вторым циклом, и это повредит ваш массив. самое простое решение - использовать другое имя для второго цикла:

foreach ($arr as &$item) { ... }

foreach ($arr as $anotherItem) { ... }
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.