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 (не знаю, де), тому що це дуже допущена помилка.
Rudie

Відповіді:


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виході петлі передпліччя не виходить за рамки? Це здається проблемою закриття?
Jocull

6
@jocull: IN PHP пропонують, тому що, в той час, і т. д. не створюють власної сфери.
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 ОНОВЛЮє змінну замість заміни. наприклад те саме, що виконувати unset ($ item) перед другим циклом. Зауважте, що unset не очищає значення (і, отже, останній елемент у масиві), він просто видаляє змінну.
Програміст

На жаль, 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.