Как на самом деле работает PHP foreach?

avatar
DaveRandom
7 апреля 2012 в 19:33
432330
7
2159

Позвольте мне префикс этого, сказав, что я знаю, что такое foreach, что он делает и как его использовать. Этот вопрос касается того, как это работает под капотом, и мне не нужны ответы типа «вот как вы зацикливаете массив с помощью foreach».


Долгое время я предполагал, что foreach работает с самим массивом. Затем я нашел много ссылок на тот факт, что он работает с копией массива, и с тех пор я предположил, что это конец истории. Но недавно я начал дискуссию по этому поводу, и после небольшого экспериментирования обнаружил, что на самом деле это не 100% правда.

Позвольте мне показать, что я имею в виду. Для следующих тестовых случаев мы будем работать со следующим массивом:

$array = array(1, 2, 3, 4, 5);

Тестовый пример 1:

foreach ($array as $item) {
  echo "$item\n";
  $array[] = $item;
}
print_r($array);

/* Output in loop:    1 2 3 4 5
   $array after loop: 1 2 3 4 5 1 2 3 4 5 */

Это ясно показывает, что мы не работаем напрямую с исходным массивом - иначе цикл продолжался бы вечно, поскольку мы постоянно помещаем элементы в массив во время цикла. Но на всякий случай:

Тестовый пример 2:

foreach ($array as $key => $item) {
  $array[$key + 1] = $item + 2;
  echo "$item\n";
}

print_r($array);

/* Output in loop:    1 2 3 4 5
   $array after loop: 1 3 4 5 6 7 */

Это подтверждает наш первоначальный вывод, мы работаем с копией исходного массива во время цикла, иначе мы увидим измененные значения во время цикла. Но ...

Если мы заглянем в руководство, мы найдем следующее утверждение:

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

Верно ... похоже, это предполагает, что foreach полагается на указатель на массив исходного массива. Но мы только что доказали, что не работаем с исходным массивом , верно? Ну, не совсем.

Тестовый пример 3:

// Move the array pointer on one to make sure it doesn't affect the loop
var_dump(each($array));

foreach ($array as $item) {
  echo "$item\n";
}

var_dump(each($array));

/* Output
  array(4) {
    [1]=>
    int(1)
    ["value"]=>
    int(1)
    [0]=>
    int(0)
    ["key"]=>
    int(0)
  }
  1
  2
  3
  4
  5
  bool(false)
*/

Итак, несмотря на то, что мы не работаем напрямую с исходным массивом, мы работаем напрямую с указателем исходного массива - тот факт, что указатель находится в конце массива в конце цикла, показывает это. Но это не может быть правдой - если бы это было так, то тестовый пример 1 будет повторяться бесконечно.

В руководстве по PHP также указано:

Поскольку foreach полагается на указатель внутреннего массива, его изменение в цикле может привести к неожиданному поведению.

Что ж, давайте выясним, что это за "неожиданное поведение" (технически любое поведение является неожиданным, поскольку я больше не знаю, чего ожидать).

Тестовый пример 4:

foreach ($array as $key => $item) {
  echo "$item\n";
  each($array);
}

/* Output: 1 2 3 4 5 */

Тестовый пример 5:

foreach ($array as $key => $item) {
  echo "$item\n";
  reset($array);
}

/* Output: 1 2 3 4 5 */

... в этом нет ничего неожиданного, на самом деле, похоже, что это подтверждает теорию "копии источника".


Вопрос

Что здесь происходит? Мой C-fu недостаточно хорош, чтобы я мог сделать правильный вывод, просто взглянув на исходный код PHP, я был бы признателен, если бы кто-нибудь мог перевести его на английский для меня.

Мне кажется, что foreach работает с копией массива, но устанавливает указатель массива исходного массива в конец массива после цикла.

  • Это правда и вся история?
  • Если нет, то что он на самом деле делает?
  • Есть ли ситуация, когда использование функций, регулирующих указатель массива (each(), reset() и др.) Во время foreach может повлиять на результат цикла?
Источник
Michael Berkowski
7 апреля 2012 в 19:40
8

@DaveRandom Там должен быть тег php-internals, но я оставлю его вам решать, какой из 5 других тегов заменить.

zb'
7 апреля 2012 в 19:40
0

попробуйте также unset($array[$key + 1]);

zb'
7 апреля 2012 в 19:43
6

выглядит как КОРОВА, без дескриптора удаления

knittl
7 апреля 2012 в 19:49
164

Сначала я подумал: «Господи, еще один вопрос для новичков». Прочтите документацию… хм, явно неопределенное поведение «. Затем я прочитал вопрос полностью и должен сказать: мне он нравится. Вы приложили немало усилий и написали все тестовые примеры. пс. тестовые случаи 4 и 5 одинаковы?

DaveRandom
7 апреля 2012 в 19:53
2

@knittl Разница между 4 + 5 составляет each() против reset() - попытка заставить его пропустить элемент или начать с начала, соответственно. Поведение, похоже, одинаково для обоих, однако указатель исходного массива игнорируется.

DaveRandom
7 апреля 2012 в 19:55
0

@eicto По-прежнему не влияет на выполнение цикла - codepad.org/y4hPzEw6. что вы имеете в виду под looks like COW... - не могли бы вы уточнить?

DaveRandom
7 апреля 2012 в 20:25
1

@eicto О, я понимаю, что вы имеете в виду, спасибо за разъяснения. Я думаю, что это, вероятно, разумное предложение, хотя я все еще не уверен, как это повлияет на указатель исходного массива. Я не думал, что вы имели в виду, что он выглядел как черно-белый рогатый скот, но это тоже хорошо ;-)

Niko
7 апреля 2012 в 20:49
23

Просто подумайте о том, почему имеет смысл касаться указателя массива: PHP необходимо сбросить и переместить указатель внутреннего массива исходного массива вместе с копией, потому что пользователь может запросить ссылку на текущее значение (foreach ($array as &$value)) - PHP должен знать текущую позицию в исходном массиве, даже если он фактически выполняет итерацию по копии.

Shaheer
10 апреля 2012 в 17:58
2

этот выглядит более запутанным codepad.org/RKFka0td

saji89
28 февраля 2013 в 06:56
1

Кстати, это не C++ -fu, вам нужно C -fu. :)

Sean
28 февраля 2013 в 07:43
3

На самом деле, @OliCharlesworth, я бы сказал, что PHP имеет лучшую документацию по любому языку программирования в мире. Есть множество причин ненавидеть PHP, но его документация к ним не относится.

Oliver Charlesworth
28 февраля 2013 в 08:43
5

@Sean: IMHO, документация по PHP действительно плохо описывает нюансы основных языковых функций. Но это, возможно, потому, что в язык встроено так много специальных случаев ...

Jon
15 апреля 2014 в 08:55
0

Вы выполняете итерацию по массиву с refcount = 1 по ссылке, поэтому сразу становится ясно, что а) копия не будет сделана и б) массив будет сделан ссылкой (удаление ссылки приведет к тому, что изменения не будут видны внутри цикла ).

Alma Do
15 апреля 2014 в 09:20
0

@monocell да, я еще раз перечитал. Но дело в том, что мне не так ясно, что одна и та же механика порядка оценки кода (и перемещения указателя) работает по-разному.

Patrick Hofman
15 апреля 2014 в 10:14
2

@AlmaDo: этот вопрос обсуждается на Meta: meta.stackexchange.com/questions/229549/…

Martin
5 декабря 2017 в 11:39
4

(технически любое поведение является неожиданным, так как я больше не знаю, чего ожидать) - великолепно

Ответы (7)

avatar
NikiC
13 февраля 2013 в 13:21
1759

foreach поддерживает итерацию по трем различным типам значений:

  • Массивы
  • Обычные объекты
  • Traversable объекты

Далее я постараюсь точно объяснить, как итерация работает в разных случаях. Безусловно, самый простой случай - это объекты Traversable, поскольку для этих объектов foreach по сути является только синтаксический сахар для кода в следующих строках:

foreach ($it as $k => $v) { /* ... */ }

/* translates to: */

if ($it instanceof IteratorAggregate) {
    $it = $it->getIterator();
}
for ($it->rewind(); $it->valid(); $it->next()) {
    $v = $it->current();
    $k = $it->key();
    /* ... */
}

Для внутренних классов фактических вызовов методов можно избежать за счет использования внутреннего API, который по сути просто отражает интерфейс Iterator на уровне C.

Итерация массивов и простых объектов значительно сложнее. Прежде всего, следует отметить, что в PHP «массивы» на самом деле являются упорядоченными словарями, и они будут перемещаться в соответствии с этим порядком (который соответствует порядку вставки, если вы не использовали что-то вроде sort). Это противоположно итерации по естественному порядку ключей (как часто работают списки на других языках) или отсутствию определенного порядка вообще (как часто работают словари на других языках).

То же самое применимо и к объектам, поскольку свойства объекта можно рассматривать как другой (упорядоченный) словарь, отображающий имена свойств свойств на их значения, плюс некоторая обработка видимости. В большинстве случаев свойства объекта фактически не сохраняются таким довольно неэффективным способом. Однако, если вы начнете перебирать объект, обычно используемое упакованное представление будет преобразовано в настоящий словарь. В этот момент итерация простых объектов становится очень похожей на итерацию массивов (вот почему я здесь особо не обсуждаю итерацию простых объектов).

Пока все хорошо. Перебирать словарь не может быть слишком сложно, правда? Проблемы начинаются, когда вы понимаете, что массив / объект может изменяться во время итерации. Это может произойти несколькими способами:

  • Если вы выполняете итерацию по ссылке с использованием foreach ($arr as &$v), то $arr превращается в ссылку, и вы можете изменить ее во время итерации.
  • В PHP 5 то же самое применяется, даже если вы выполняете итерацию по значению, но массив заранее был ссылкой: $ref =& $arr; foreach ($ref as $v)
  • Объекты имеют семантику передачи обходного дескриптора, которая для большинства практических целей означает, что они ведут себя как ссылки. Таким образом, объекты всегда можно изменить во время итерации.

Проблема с разрешением изменений во время итерации - это случай, когда элемент, на котором вы сейчас находитесь, удален. Допустим, вы используете указатель, чтобы отслеживать, в каком элементе массива вы находитесь в данный момент. Если этот элемент теперь освобожден, у вас останется висящий указатель (обычно это приводит к segfault).

Есть разные способы решения этой проблемы. В этом отношении PHP 5 и PHP 7 существенно различаются, и ниже я опишу оба поведения. Резюмируя, можно сказать, что подход PHP 5 был довольно глупым и приводил ко всем видам странных краевых проблем, в то время как более активный подход PHP 7 приводит к более предсказуемому и последовательному поведению.

Напоследок следует отметить, что PHP использует подсчет ссылок и копирование при записи для управления памятью. Это означает, что если вы «копируете» значение, вы фактически просто повторно используете старое значение и увеличиваете его счетчик ссылок (refcount). Только после того, как вы выполните какую-либо модификацию, будет сделана настоящая копия (называемая «дублированием»). См. , где вас обманывают, для более подробного введения по этой теме.

PHP 5

Внутренний указатель массива и HashPointer

Массивы в PHP 5 имеют один выделенный «внутренний указатель на массив» (IAP), который должным образом поддерживает модификации: всякий раз, когда элемент удаляется, будет проверяться, указывает ли IAP на этот элемент. Если это так, вместо этого выполняется переход к следующему элементу.

Хотя foreach действительно использует IAP, возникает дополнительная сложность: существует только один IAP, но один массив может быть частью нескольких циклов foreach:

// Using by-ref iteration here to make sure that it's really
// the same array in both loops and not a copy
foreach ($arr as &$v1) {
    foreach ($arr as &$v) {
        // ...
    }
}

Для поддержки двух одновременных циклов только с одним внутренним указателем массива foreach выполняет следующие махинации: перед выполнением тела цикла foreach создает резервную копию указателя на текущий элемент и его хэш в каждом цикле каждого цикла. HashPointer. После выполнения тела цикла IAP вернется к этому элементу, если он все еще существует. Однако, если элемент был удален, мы просто будем использовать его там, где в данный момент находится IAP. Эта схема в некотором роде работает, но есть много странного поведения, которое вы можете избежать, некоторые из которых я продемонстрирую ниже.

Дублирование массива

IAP - это видимая функция массива (предоставляемая через семейство функций current), поскольку такие изменения в IAP считаются модификациями в соответствии с семантикой копирования при записи. К сожалению, это означает, что foreach во многих случаях вынужден дублировать массив, по которому выполняется итерация. Точные условия:

  1. Массив не является ссылкой (is_ref = 0). Если это ссылка, то изменения в ней предполагается для распространения, поэтому ее не следует дублировать.
  2. У массива refcount> 1. Если refcount равно 1, то массив не является общим, и мы можем изменить его напрямую.

Если массив не дублируется (is_ref = 0, refcount = 1), то будет увеличиваться только его refcount (*). Кроме того, если используется foreach по ссылке, то (потенциально дублированный) массив будет преобразован в ссылку.

Рассмотрим этот код как пример дублирования:

function iterate($arr) {
    foreach ($arr as $v) {}
}

$outerArr = [0, 1, 2, 3, 4];
iterate($outerArr);

Здесь $arr будет продублирован, чтобы предотвратить утечку изменений IAP на $arr на $outerArr. В условиях приведенных выше условий массив не является ссылкой (is_ref = 0) и используется в двух местах (refcount = 2). Это требование неудачно и является артефактом неоптимальной реализации (здесь нет никаких проблем с модификацией во время итерации, поэтому нам действительно не нужно использовать IAP в первую очередь).

(*) Увеличение refcount здесь звучит безобидно, но нарушает семантику копирования при записи (COW): это означает, что мы собираемся изменить IAP массива refcount = 2, в то время как COW диктует, что модификации могут выполняется только для значений refcount = 1. Это нарушение приводит к видимому пользователю изменению поведения (в то время как COW обычно прозрачен), потому что изменение IAP в повторяемом массиве будет наблюдаемым - но только до первой модификации массива, не связанной с IAP. Вместо этого были бы три «действительных» варианта: а) всегда дублировать, б) не увеличивать refcount и, таким образом, позволять произвольно изменять повторяемый массив в цикле, или в) вообще не использовать IAP (решение PHP 7).

Порядок продвижения позиции

Есть одна последняя деталь реализации, о которой вы должны знать, чтобы правильно понимать примеры кода ниже. «Обычный» способ перебора некоторой структуры данных в псевдокоде выглядел бы примерно так:

reset(arr);
while (get_current_data(arr, &data) == SUCCESS) {
    code();
    move_forward(arr);
}

Однако foreach, будучи довольно особенной снежинкой, предпочитает действовать немного иначе:

reset(arr);
while (get_current_data(arr, &data) == SUCCESS) {
    move_forward(arr);
    code();
}

А именно, указатель массива уже перемещен вперед до выполнения тела цикла. Это означает, что пока тело цикла работает с элементом $i, IAP уже находится в элементе $i+1. Это причина, по которой образцы кода, показывающие модификацию во время итерации, всегда будут unset следующим элементом , а не текущим.

Примеры: ваши тестовые случаи

Три описанных выше аспекта должны дать вам в основном полное представление об особенностях реализации foreach, и мы можем перейти к обсуждению некоторых примеров.

На этом этапе легко объяснить поведение ваших тестовых примеров:

  • В тестовых примерах 1 и 2 $array начинается с refcount = 1, поэтому он не будет дублироваться на foreach: увеличивается только refcount. Когда тело цикла впоследствии модифицирует массив (который в этой точке имеет refcount = 2), в этой точке произойдет дублирование. Foreach продолжит работу с неизмененной копией $array.

  • В тестовом примере 3 снова массив не дублируется, поэтому foreach будет изменять IAP переменной $array. В конце итерации IAP равен NULL (что означает, что итерация завершена), что each указывает, возвращая false.

  • В тестовых примерах 4 и 5 как each, так и reset являются ссылочными функциями. $array имеет refcount=2, когда он передается им, поэтому он должен быть продублирован. Таким образом, foreach снова будет работать с отдельным массивом.

Примеры: эффекты current в foreach

Хороший способ показать различные варианты дублирования - это наблюдать за поведением функции current() внутри цикла foreach. Рассмотрим этот пример:

foreach ($array as $val) {
    var_dump(current($array));
}
/* Output: 2 2 2 2 2 */

Здесь вы должны знать, что current() - это функция по ссылке (на самом деле: предпочитать ссылку), даже если она не изменяет массив. Это должно быть для того, чтобы хорошо работать со всеми другими функциями, такими как next, которые все являются ссылочными. Передача по ссылке подразумевает, что массив должен быть разделен, и поэтому $array и foreach-array будут разными. Причина, по которой вы получаете 2 вместо 1, также упоминается выше: foreach перемещает указатель массива до выполнения кода пользователя, а не после. Таким образом, хотя код находится в первом элементе, foreach уже переместил указатель на второй.

Теперь попробуем небольшую модификацию:

$ref = &$array;
foreach ($array as $val) {
    var_dump(current($array));
}
/* Output: 2 3 4 5 false */

Здесь у нас есть случай is_ref = 1, поэтому массив не копируется (как и выше). Но теперь, когда это ссылка, массив больше не нужно дублировать при переходе к функции by-ref current(). Таким образом, current() и foreach работают с одним и тем же массивом. Тем не менее, вы по-прежнему видите поведение «поодиночке» из-за того, как foreach перемещает указатель.

Вы получаете то же поведение при выполнении итерации по ссылке:

foreach ($array as &$val) {
    var_dump(current($array));
}
/* Output: 2 3 4 5 false */

Здесь важно то, что foreach сделает $array is_ref = 1 при повторении по ссылке, поэтому в основном у вас такая же ситуация, как и выше.

Еще один небольшой вариант, на этот раз мы назначим массив другой переменной:

$foo = $array;
foreach ($array as $val) {
    var_dump(current($array));
}
/* Output: 1 1 1 1 1 */

Здесь счетчик ссылок для $array равен 2, когда цикл запущен, поэтому на этот раз нам действительно нужно выполнить дублирование заранее. Таким образом, $array и массив, используемый foreach, будут полностью отделены от самого начала. Вот почему вы получаете позицию IAP, где бы она ни находилась до цикла (в данном случае это была первая позиция).

Примеры: изменение во время итерации

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

Рассмотрим эти вложенные циклы по одному и тому же массиву (где итерация по ссылке используется, чтобы убедиться, что это действительно один и тот же):

foreach ($array as &$v1) {
    foreach ($array as &$v2) {
        if ($v1 == 1 && $v2 == 1) {
            unset($array[1]);
        }
        echo "($v1, $v2)\n";
    }
}

// Output: (1, 1) (1, 3) (1, 4) (1, 5)

Ожидаемая часть здесь состоит в том, что (1, 2) отсутствует в выходных данных, поскольку элемент 1 был удален. Что, вероятно, неожиданно, так это то, что внешний цикл останавливается после первого элемента. Почему это так?

Причиной этого является описанный выше хак с вложенным циклом: перед запуском тела цикла текущая позиция IAP и хэш копируются в HashPointer. После тела цикла он будет восстановлен, но только если элемент все еще существует, в противном случае вместо него используется текущая позиция IAP (какой бы она ни была). В приведенном выше примере это именно так: текущий элемент внешнего цикла был удален, поэтому он будет использовать IAP, который уже был отмечен внутренним циклом как завершенный!

Другое следствие механизма HashPointer резервное копирование + восстановление состоит в том, что изменения IAP через reset() и т. Д. Обычно не влияют на foreach. Например, следующий код выполняется так, как если бы reset() вообще не присутствовал:

$array = [1, 2, 3, 4, 5];
foreach ($array as &$value) {
    var_dump($value);
    reset($array);
}
// output: 1, 2, 3, 4, 5

Причина в том, что, хотя reset() временно изменяет IAP, он будет восстановлен до текущего элемента foreach после тела цикла. Чтобы заставить reset() воздействовать на цикл, вы должны дополнительно удалить текущий элемент, чтобы механизм резервного копирования / восстановления отказал:

$array = [1, 2, 3, 4, 5];
$ref =& $array;
foreach ($array as $value) {
    var_dump($value);
    unset($array[1]);
    reset($array);
}
// output: 1, 1, 3, 4, 5

Но эти примеры все еще вменяемы. Настоящее веселье начинается, если вы помните, что восстановление HashPointer использует указатель на элемент и его хэш, чтобы определить, существует ли он еще. Но: у хэшей есть коллизии, и указатели можно использовать повторно! Это означает, что при тщательном выборе ключей массива мы можем заставить foreach поверить, что удаленный элемент все еще существует, поэтому он перейдет непосредственно к нему. Пример:

$array = ['EzEz' => 1, 'EzFY' => 2, 'FYEz' => 3];
$ref =& $array;
foreach ($array as $value) {
    unset($array['EzFY']);
    $array['FYFY'] = 4;
    reset($array);
    var_dump($value);
}
// output: 1, 4

Здесь обычно следует ожидать вывода 1, 1, 3, 4 в соответствии с предыдущими правилами. Как происходит то, что 'FYFY' имеет тот же хэш, что и удаленный элемент 'EzFY', и распределитель повторно использует то же место в памяти для хранения элемента. Таким образом, foreach завершает прямой переход к вновь вставленному элементу, сокращая таким образом цикл.

Замена повторяемого объекта во время цикла

Последний странный случай, о котором я хотел бы упомянуть, это то, что PHP позволяет вам заменять повторяющийся объект во время цикла. Таким образом, вы можете начать итерацию с одним массивом, а затем заменить его другим массивом на полпути. Или начните итерацию по массиву, а затем замените его объектом:

$arr = [1, 2, 3, 4, 5];
$obj = (object) [6, 7, 8, 9, 10];

$ref =& $arr;
foreach ($ref as $val) {
    echo "$val\n";
    if ($val == 3) {
        $ref = $obj;
    }
}
/* Output: 1 2 3 6 7 8 9 10 */

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

PHP 7

Итераторы хеш-таблицы

Если вы все еще помните, основная проблема с итерацией массива заключалась в том, как обрабатывать удаление элементов в середине итерации. В PHP 5 для этой цели использовался единственный внутренний указатель массива (IAP), что было несколько неоптимально, так как один указатель массива приходилось растягивать для поддержки нескольких одновременных циклов foreach и , взаимодействие с reset() и т. Д. Сверху. из этого.

PHP 7 использует другой подход, а именно поддерживает создание произвольного количества внешних безопасных итераторов хеш-таблицы. Эти итераторы должны быть зарегистрированы в массиве, с этого момента они имеют ту же семантику, что и IAP: если элемент массива удален, все итераторы хэш-таблицы, указывающие на этот элемент, будут перемещены к следующему элементу.

Это означает, что foreach больше не будет использовать IAP вообще . Цикл foreach не окажет абсолютно никакого влияния на результаты current() и т. Д., И его собственное поведение никогда не будет зависеть от таких функций, как reset() и т. Д.

Дублирование массива

Еще одно важное изменение между PHP 5 и PHP 7 связано с дублированием массивов. Теперь, когда IAP больше не используется, итерация массива по значению будет выполнять только приращение refcount (вместо дублирования массива) во всех случаях. Если массив изменяется во время цикла foreach, в этот момент произойдет дублирование (в соответствии с копированием при записи), и foreach продолжит работать со старым массивом.

В большинстве случаев это изменение прозрачно и не имеет никакого другого эффекта, кроме повышения производительности. Однако есть один случай, когда это приводит к другому поведению, а именно в случае, когда массив был предварительно ссылкой:

$array = [1, 2, 3, 4, 5];
$ref = &$array;
foreach ($array as $val) {
    var_dump($val);
    $array[2] = 0;
}
/* Old output: 1, 2, 0, 4, 5 */
/* New output: 1, 2, 3, 4, 5 */

Раньше итерация ссылочных массивов по значению была особым случаем. В этом случае дублирования не произошло, поэтому все изменения массива во время итерации будут отражены в цикле. В PHP 7 этого особого случая нет: итерация массива по значению всегда продолжает работать с исходными элементами, игнорируя любые изменения во время цикла.

Это, конечно, не относится к итерации по ссылке. Если вы выполняете итерацию по ссылке, все изменения будут отражены в цикле. Интересно, что то же самое верно и для итерации простых объектов по значению:

$obj = new stdClass;
$obj->foo = 1;
$obj->bar = 2;
foreach ($obj as $val) {
    var_dump($val);
    $obj->bar = 42;
}
/* Old and new output: 1, 42 */

Это отражает семантику объектов по дескрипторам (т.е. они ведут себя как ссылки даже в контекстах по значению).

Примеры

Давайте рассмотрим несколько примеров, начиная с ваших тестовых случаев:

  • Тестовые случаи 1 и 2 сохраняют тот же результат: итерация массива по значению всегда продолжает работать с исходными элементами. (В этом случае даже refcounting и поведение дублирования точно такое же между PHP 5 и PHP 7).

  • Изменения в тестовом примере 3: Foreach больше не использует IAP, поэтому цикл не влияет на each(). Он будет иметь одинаковый вывод до и после.

  • Тестовые примеры 4 и 5 остаются неизменными: each() и reset() дублируют массив перед изменением IAP, в то время как foreach по-прежнему использует исходный массив. (Не то чтобы изменение IAP имело значение, даже если бы массив был общим.)

Второй набор примеров был связан с поведением current() в различных конфигурациях reference/refcounting. Это больше не имеет смысла, поскольку current() полностью не зависит от цикла, поэтому его возвращаемое значение всегда остается неизменным.

Однако мы получаем некоторые интересные изменения при рассмотрении модификаций во время итерации. Я надеюсь, что вы сочтете новое поведение более разумным. Первый пример:

$array = [1, 2, 3, 4, 5];
foreach ($array as &$v1) {
    foreach ($array as &$v2) {
        if ($v1 == 1 && $v2 == 1) {
            unset($array[1]);
        }
        echo "($v1, $v2)\n";
    }
}

// Old output: (1, 1) (1, 3) (1, 4) (1, 5)
// New output: (1, 1) (1, 3) (1, 4) (1, 5)
//             (3, 1) (3, 3) (3, 4) (3, 5)
//             (4, 1) (4, 3) (4, 4) (4, 5)
//             (5, 1) (5, 3) (5, 4) (5, 5) 

Как видите, внешний цикл больше не прерывается после первой итерации. Причина в том, что у обоих циклов теперь есть полностью отдельные итераторы хэш-таблицы, и больше нет перекрестного заражения обоих циклов через общий IAP.

Еще один странный крайний случай, который сейчас исправлен, - это странный эффект, который вы получаете, когда удаляете и добавляете элементы, которые имеют одинаковый хэш:

$array = ['EzEz' => 1, 'EzFY' => 2, 'FYEz' => 3];
foreach ($array as &$value) {
    unset($array['EzFY']);
    $array['FYFY'] = 4;
    var_dump($value);
}
// Old output: 1, 4
// New output: 1, 3, 4

Раньше механизм восстановления HashPointer переходил прямо к новому элементу, потому что он «выглядел» так, как будто он был таким же, как удаленный элемент (из-за столкновения хеша и указателя). Поскольку мы больше ни в чем не полагаемся на хэш элемента, это больше не проблема.

NikiC
13 февраля 2013 в 14:25
4

@ Баба. Передача его в функцию аналогична выполнению $foo = $array перед циклом;)

Arnaud Le Blanc
27 февраля 2013 в 21:12
0

Кажется, что foreach сохраняет позицию массива в структуре HashPointer после итерации и восстанавливает ее перед итерацией. Структура содержит HashPosition и h сегмента, на который указывает HashPosition. Хэш используется для проверки того, что сегмент все еще находится в таблице, поэтому HashPointer можно безопасно использовать даже после модификации массива. Вероятно, поэтому мы можем видеть измененную позицию массива (each() возвращает null после foreach), а также почему попытка его изменения не влияет на foreach.

NikiC
27 февраля 2013 в 21:48
0

@ arnaud576875 Хорошее замечание, я забыл об этом упомянуть. Хотя я не понимаю, как это связано с «видеть измененную позицию массива (каждый () возвращает null после foreach)». Это просто эффект от использования IAP. Afaik это просто для предотвращения изменения позиции вызовов next / prev в теле foreach (в случае ref).

shu zOMG chen
27 февраля 2013 в 22:30
35

Для тех из вас, кто не знает, что такое звал, см. Сары Гоулман> blog.golemon.com/2007/01/youre-being-lied-to.html

NikiC
27 февраля 2013 в 22:37
0

@ arnaud576875 Я добавил некоторые пояснения (и даже больше странностей!) относительно HashPointer в конце (начиная с последнего заголовка)

unbeli
1 марта 2013 в 09:36
1

Незначительное исправление: то, что вы называете Bucket, не является тем, что обычно называется Bucket в хеш-таблице. Обычно Bucket - это набор записей с одинаковым размером% хэша. Кажется, вы используете его для того, что обычно называется записью. Связанный список не по сегментам, а по записям.

NikiC
1 марта 2013 в 14:47
13

@unbeli Я использую терминологию, используемую внутри PHP. Bucket являются частью двусвязного списка для хеш-коллизий, а также частью двусвязного списка для порядка;)

lud
31 марта 2016 в 09:04
5

Отличный ответ. Я думаю, вы имели в виду iterate($outerArr);, а не iterate($arr); где-то.

Denise Ignatova
24 декабря 2020 в 09:59
0

Лучший ответ. Спасибо @NikiC

avatar
Pranav Rana
13 ноября 2017 в 14:08
8

Каждый цикл PHP можно использовать с Indexed arrays, Associative arrays и Object public variables.

В цикле foreach первое, что делает php, - это создает копию массива, который нужно повторить. Затем PHP выполняет итерацию по этому новому массиву copy, а не по исходному. Это показано в следующем примере:

<?php
$numbers = [1,2,3,4,5,6,7,8,9]; # initial values for our array
echo '<pre>', print_r($numbers, true), '</pre>', '<hr />';
foreach($numbers as $index => $number){
    $numbers[$index] = $number + 1; # this is making changes to the origial array
    echo 'Inside of the array = ', $index, ': ', $number, '<br />'; # showing data from the copied array
}
echo '<hr />', '<pre>', print_r($numbers, true), '</pre>'; # shows the original values (also includes the newly added values).

Помимо этого, php также позволяет использовать iterated values as a reference to the original array value. Это показано ниже:

<?php
$numbers = [1,2,3,4,5,6,7,8,9];
echo '<pre>', print_r($numbers, true), '</pre>';
foreach($numbers as $index => &$number){
    ++$number; # we are incrementing the original value
    echo 'Inside of the array = ', $index, ': ', $number, '<br />'; # this is showing the original value
}
echo '<hr />';
echo '<pre>', print_r($numbers, true), '</pre>'; # we are again showing the original value

Примечание: Он не позволяет использовать original array indexes как references.

Источник: http://dwellupper.io/post/47/understanding-php-foreach-loop-with-examples

Christian
26 декабря 2017 в 21:35
1

Object public variables неверно или, в лучшем случае, вводит в заблуждение. Вы не можете использовать объект в массиве без правильного интерфейса (например, Traversible), и когда вы делаете foreach((array)$obj ..., вы фактически работаете с простым массивом, а не с объектом.

avatar
Hrvoje Antunović
21 апреля 2017 в 08:44
14

Отличный вопрос, потому что многие разработчики, даже опытные, сбиты с толку тем, как PHP обрабатывает массивы в циклах foreach. В стандартном цикле foreach PHP создает копию массива, который используется в цикле. Копия удаляется сразу после завершения цикла. Это прозрачно в работе простого цикла foreach. Например:

$set = array("apple", "banana", "coconut");
foreach ( $set AS $item ) {
    echo "{$item}\n";
}

Это выводит:

apple
banana
coconut

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

$set = array("apple", "banana", "coconut");
foreach ( $set AS $item ) {
    $item = strrev ($item);
}

print_r($set);

Это выводит:

Array
(
    [0] => apple
    [1] => banana
    [2] => coconut
)

Любые изменения от оригинала не могут быть уведомлены, фактически нет никаких изменений от оригинала, даже если вы явно присвоили значение $ item. Это потому, что вы работаете с $ item в том виде, в котором он отображается в копии $ set, над которой работаете. Вы можете переопределить это, взяв $ item по ссылке, например:

$set = array("apple", "banana", "coconut");
foreach ( $set AS &$item ) {
    $item = strrev($item);
}
print_r($set);

Это выводит:

Array
(
    [0] => elppa
    [1] => ananab
    [2] => tunococ
)

Таким образом, очевидно и наблюдаемо, что когда $ item работает по ссылке, изменения, внесенные в $ item, вносятся в элементы исходного $ set. Использование $ item по ссылке также не позволяет PHP создавать копию массива. Чтобы проверить это, сначала мы покажем быстрый скрипт, демонстрирующий копию:

$set = array("apple", "banana", "coconut");
foreach ( $set AS $item ) {
    $set[] = ucfirst($item);
}
print_r($set);

Это выводит:

Array
(
    [0] => apple
    [1] => banana
    [2] => coconut
    [3] => Apple
    [4] => Banana
    [5] => Coconut
)

Как показано в примере, PHP скопировал $ set и использовал его для выполнения цикла, но когда $ set использовался внутри цикла, PHP добавил переменные в исходный массив, а не в скопированный массив. По сути, PHP использует только скопированный массив для выполнения цикла и присвоения $ item. Из-за этого вышеупомянутый цикл выполняется только 3 раза, и каждый раз он добавляет другое значение в конец исходного $ set, оставляя исходный $ set с 6 элементами, но никогда не входя в бесконечный цикл.

Однако что, если бы мы использовали $ item по ссылке, как я упоминал ранее? В приведенный выше тест добавлен один символ:

$set = array("apple", "banana", "coconut");
foreach ( $set AS &$item ) {
    $set[] = ucfirst($item);
}
print_r($set);

Возникает бесконечный цикл. Обратите внимание, что на самом деле это бесконечный цикл, вам придется либо убить сценарий самостоятельно, либо подождать, пока у вашей ОС не закончится память. Я добавил в свой скрипт следующую строку, чтобы у PHP очень быстро не хватало памяти, я предлагаю вам сделать то же самое, если вы собираетесь запускать эти тесты с бесконечным циклом:

ini_set("memory_limit","1M");

Итак, в этом предыдущем примере с бесконечным циклом мы видим причину, по которой PHP был написан для создания копии массива для выполнения цикла. Когда копия создается и используется только структурой самой конструкции цикла, массив остается статическим на протяжении всего выполнения цикла, поэтому вы никогда не столкнетесь с проблемами.

avatar
user3535130
15 апреля 2014 в 09:32
16

Согласно документации, предоставленной в руководстве по PHP.

На каждой итерации значение текущего элемента присваивается $ v и внутреннему
указатель массива продвигается вперед на единицу (поэтому на следующей итерации вы будете смотреть на следующий элемент).

Итак, как в вашем первом примере:

$array = ['foo'=>1];
foreach($array as $k=>&$v)
{
   $array['bar']=2;
   echo($v);
}

$array имеет только один элемент, поэтому в соответствии с выполнением foreach 1 присваивается $v, и у него нет другого элемента для перемещения указателя

Но во втором примере:

$array = ['foo'=>1, 'bar'=>2];
foreach($array as $k=>&$v)
{
   $array['baz']=3;
   echo($v);
}

$array имеет два элемента, поэтому теперь $ array оценивает нулевые индексы и перемещает указатель на единицу. Для первой итерации цикла добавлено $array['baz']=3; как передача по ссылке.

avatar
dkasipovic
15 апреля 2014 в 08:46
39

ПРИМЕЧАНИЕ ДЛЯ PHP 7

Чтобы обновить этот ответ, поскольку он приобрел некоторую популярность: этот ответ больше не применяется с PHP 7. Как объясняется в «Обратно несовместимые изменения», в PHP 7 foreach работает с копией array, поэтому любые изменения самого массива не отражаются в цикле foreach. Подробнее по ссылке.

Пояснение (цитата из php.net):

Первая форма перебирает массив, заданный выражением array_expression. На каждого итерация, значение текущего элемента присваивается $ value и указатель внутреннего массива продвигается на единицу (поэтому на следующем итерация, вы увидите следующий элемент).

Итак, в вашем первом примере у вас есть только один элемент в массиве, и когда указатель перемещается, следующий элемент не существует, поэтому после добавления нового элемента foreach заканчивается, потому что он уже «решил», что он является последний элемент.

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

Я считаю, что это все следствие На каждой итерации части объяснения в документации, что, вероятно, означает, что foreach выполняет всю логику перед вызовом кода в {}.

Тестовый пример

Если вы запустите это:

<?
    $array = Array(
        'foo' => 1,
        'bar' => 2
    );
    foreach($array as $k=>&$v) {
        $array['baz']=3;
        echo $v." ";
    }
    print_r($array);
?>

Вы получите следующий результат:

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

Это означает, что он принял модификацию и прошел через нее, потому что она была изменена «вовремя». Но если вы сделаете это:

<?
    $array = Array(
        'foo' => 1,
        'bar' => 2
    );
    foreach($array as $k=>&$v) {
        if ($k=='bar') {
            $array['baz']=3;
        }
        echo $v." ";
    }
    print_r($array);
?>

Вы получите:

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

Это означает, что массив был изменен, но, поскольку мы изменили его, когда foreach уже находился в последнем элементе массива, он «решил» больше не выполнять цикл, и хотя мы добавили новый элемент, мы добавили его. "слишком поздно", и это не было выполнено шлейфом.

Подробное объяснение можно прочитать по адресу Как на самом деле работает PHP foreach?, в котором объясняются внутренние механизмы этого поведения.

dkasipovic
15 апреля 2014 в 08:49
7

Хорошо, вы прочитали остальную часть ответа? Совершенно логично, что foreach решает, будет ли он повторяться в следующий раз перед тем, как он даже запустит в нем код.

dkasipovic
15 апреля 2014 в 08:55
2

Нет, массив изменен, но «слишком поздно», поскольку foreach уже «думает», что он находится в последнем элементе (который находится в начале итерации) и больше не будет зацикливаться. Во втором примере он не находится в последнем элементе в начале итерации и вычисляется снова в начале следующей итерации. Я пытаюсь подготовить тестовый пример.

bwoebi
15 апреля 2014 в 09:21
1

@AlmaDo Посмотрите на lxr.php.net/xref/PHP_TRUNK/Zend/zend_vm_def.h#4509 Он всегда устанавливается на следующий указатель при выполнении итерации. Итак, когда он достигнет последней итерации, он будет помечен как завершенный (с помощью указателя NULL). Когда вы затем добавляете ключ на последней итерации, foreach этого не заметит.

dkasipovic
15 апреля 2014 в 09:22
0

@AlmaDo, если вам нужны внутренние причины, прочтите их из принятого ответа на coderhelper.com/questions/10057671/how-foreach-actually-works, и я обновлю свой ответ, включив это.

Alma Do
15 апреля 2014 в 09:25
1

@DKasipovic нет. Здесь нет полного и ясного объяснения (по крайней мере, на данный момент - может быть, я ошибаюсь)

dkasipovic
15 апреля 2014 в 09:34
0

Я не согласен, но решать вам. В другом ответе и в моем ответе все объяснено. Даже мой пример подтверждает то, что я сказал. Я не понимаю, в чем проблема. Может, я вас не очень хорошо понимаю.

bwoebi
15 апреля 2014 в 09:36
4

На самом деле кажется, что @AlmaDo неправильно понимает свою логику ... Ваш ответ хорош.

ilicmsreten
1 июня 2018 в 11:53
0

@Damir, можете ли вы обновить свой ответ, потому что ваш второй пример дает тот же результат, что и первый для версии PHP> = 7

dkasipovic
1 июня 2018 в 11:56
0

@circleandsquare в "Обратно несовместимых изменениях" (secure.php.net/manual/en/migration70.incompatible.php) есть объяснение того, как foreach работает в PHP 7, я думаю, это объясняет довольно хорошо. По сути, в PHP 7 foreach работает с копией массива, поэтому любые изменения самого массива не отражаются в цикле foreach.

ilicmsreten
1 июня 2018 в 12:21
1

@Damir Я согласен с вами по поводу ответа, потому что это один из самых посещаемых вопросов, я предлагаю вам улучшить свой ответ, упомянув версию PHP, потому что это запутает новые.

avatar
sakhunzai
7 апреля 2012 в 21:03
54

Некоторые моменты, на которые следует обратить внимание при работе с foreach():

a) foreach работает на разведанной копии исходного массива. Это означает, что foreach() будет иметь ОБЩЕЕ хранилище данных до тех пор, пока не будет prospected copy не создано foreach Заметки / комментарии пользователей.

б) Что вызывает разведанную копию ? Предполагаемая копия создается на основе политики copy-on-write, то есть всякий раз, когда массив, переданный в foreach(), изменяется, создается клон исходного массива.

c) Исходный массив и итератор foreach() будут иметь DISTINCT SENTINEL VARIABLES, то есть один для исходного массива, а другой для foreach; см. тестовый код ниже. SPL, Итераторы и Итератор массива.

Вопрос о переполнении стека Как убедиться, что значение сбрасывается в цикле foreach в PHP? рассматривает случаи (3,4,5) вашего вопроса.

В следующем примере показано, что каждый () и reset () НЕ влияют на переменные SENTINEL (for example, the current index variable) итератора foreach().

$array = array(1, 2, 3, 4, 5);

list($key2, $val2) = each($array);
echo "each() Original (outside): $key2 => $val2<br/>";

foreach($array as $key => $val){
    echo "foreach: $key => $val<br/>";

    list($key2,$val2) = each($array);
    echo "each() Original(inside): $key2 => $val2<br/>";

    echo "--------Iteration--------<br/>";
    if ($key == 3){
        echo "Resetting original array pointer<br/>";
        reset($array);
    }
}

list($key2, $val2) = each($array);
echo "each() Original (outside): $key2 => $val2<br/>";

Вывод:

each() Original (outside): 0 => 1
foreach: 0 => 1
each() Original(inside): 1 => 2
--------Iteration--------
foreach: 1 => 2
each() Original(inside): 2 => 3
--------Iteration--------
foreach: 2 => 3
each() Original(inside): 3 => 4
--------Iteration--------
foreach: 3 => 4
each() Original(inside): 4 => 5
--------Iteration--------
Resetting original array pointer
foreach: 4 => 5
each() Original(inside): 0=>1
--------Iteration--------
each() Original (outside): 1 => 2
linepogl
8 апреля 2012 в 06:34
2

Ваш ответ не совсем правильный. foreach работает с потенциальной копией массива, но не создает фактическую копию, если она не нужна.

sakhunzai
8 апреля 2012 в 16:05
0

Вы хотели бы продемонстрировать, как и когда эта потенциальная копия создается с помощью кода? Мой код демонстрирует, что foreach копирует массив в 100% случаев. Я очень хочу знать. Спасибо за ваши комментарии

linepogl
9 апреля 2012 в 18:46
0

Копирование массива стоит дорого. Попробуйте подсчитать время, необходимое для перебора массива из 100000 элементов, используя for или foreach. Вы не увидите значительной разницы между ними, потому что фактического копирования не происходит.

sakhunzai
9 апреля 2012 в 19:44
0

Тогда я бы предположил, что существует SHARED data storage зарезервировано до или до copy-on-write, но (из моего фрагмента кода) очевидно, что всегда будет ДВА набора из SENTINEL variables, один для original array и другой для foreach . Спасибо, что имеет смысл

Peter Mortensen
15 апреля 2014 в 15:28
0

"разведанный"? Вы имеете в виду «защищенный»?

sakhunzai
16 апреля 2014 в 06:21
1

да, это «предполагаемая» копия, то есть «потенциальная» копия. Она не защищена, как вы предложили

avatar
linepogl
7 апреля 2012 в 20:43
123

В примере 3 вы не изменяете массив. Во всех других примерах вы изменяете либо содержимое, либо указатель внутреннего массива. Это важно, когда речь идет о массивах PHP из-за семантики оператора присваивания.

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

Вот пример:

$a = array(1,2,3);
$b = $a;  // This is lazy cloning of $a. For the time
          // being $a and $b point to the same internal
          // data structure.

$a[] = 3; // Here $a changes, which triggers the actual
          // cloning. From now on, $a and $b are two
          // different data structures. The same would
          // happen if there were a change in $b.

Возвращаясь к вашим тестовым примерам, вы легко можете представить, что foreach создает какой-то итератор со ссылкой на массив. Эта ссылка работает точно так же, как переменная $b в моем примере. Однако итератор вместе со ссылкой действуют только во время цикла, а затем они оба отбрасываются. Теперь вы можете видеть, что во всех случаях, кроме 3, массив изменяется во время цикла, в то время как эта дополнительная ссылка активна. Это вызывает клонирование, и это объясняет, что здесь происходит!

Вот отличная статья о другом побочном эффекте этого поведения копирования при записи: Тернарный оператор PHP: быстро или нет?

zb'
7 апреля 2012 в 21:35
0

кажется, вы правы, я привел пример, демонстрирующий следующее: codepad.org/OCjtvu8r одно отличие от вашего примера - он не копируется, если вы меняете значение, только если меняете ключи.

DaveRandom
8 апреля 2012 в 15:59
1

Это действительно объясняет все поведение, показанное выше, и это может быть хорошо проиллюстрировано вызовом each() в конце первого тестового примера, где мы видим, что указатель массива исходного массива указывает на второй элемент, поскольку массив был изменен во время первой итерации. Это также, кажется, демонстрирует, что foreach перемещает указатель массива перед выполнением блока кода цикла, чего я не ожидал - я бы подумал, что он сделает это в конце. Большое спасибо, это хорошо меня проясняет.