Позвольте мне префикс этого, сказав, что я знаю, что такое 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 может повлиять на результат цикла?
2174
DaveRandom 7 Апр 2012 в 23:33
8
Там, вероятно, должен быть тег php-internals, но я оставлю вам решать, какой, если любой из 5 других тегов для замены.
 – 
Michael Berkowski
7 Апр 2012 в 23:40
6
Выглядит как КОРОВА, без дескриптора удаления
 – 
zb'
7 Апр 2012 в 23:43
165
Сначала я подумал: «Господи, еще один вопрос для новичков». Прочтите документацию… хм, явно неопределенное поведение «. Затем я прочитал вопрос полностью и должен сказать: мне он нравится. Вы приложили немало усилий и написали все тестовые примеры. пс. тестовые случаи 4 и 5 одинаковы?
 – 
knittl
7 Апр 2012 в 23:49
23
Просто подумайте о том, почему имеет смысл касаться указателя массива: PHP необходимо сбросить и переместить указатель внутреннего массива исходного массива вместе с копией, потому что пользователь может запросить ссылку на текущее значение (foreach ($array as &$value)) - PHP должен знать текущую позицию в исходном массиве, даже если он фактически выполняет итерацию по копии.
 – 
Niko
8 Апр 2012 в 00:49
5
@Sean: IMHO, документация по PHP действительно плохо описывает нюансы основных языковых функций. Но это, возможно, потому, что в язык встроено так много специальных случаев ...
 – 
Oliver Charlesworth
28 Фев 2013 в 12:43

7 ответов

Лучший ответ

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 создает резервную копию указателя на текущий элемент и его хэш в отдельном 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() - это функция по ссылке (на самом деле: Preferred-ref), даже если она не изменяет массив. Это должно быть для того, чтобы хорошо работать со всеми другими функциями, такими как 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 переходил прямо к новому элементу, потому что он «выглядел» так, как будто он был таким же, как удаленный элемент (из-за столкновения хэша и указателя). Поскольку мы больше ни в чем не полагаемся на хэш элемента, это больше не проблема.

1760
Quolonel Questions 2 Окт 2019 в 00:32
4
Оно делает. Передача его функции аналогична выполнению $foo = $array перед циклом;)
 – 
NikiC
13 Фев 2013 в 18:25
35
Для тех из вас, кто не знает, что такое zval, см. blog.golemon.com/2007/01/youre-being-lied-to.html
 – 
shu zOMG chen
28 Фев 2013 в 02:30
1
Незначительное исправление: то, что вы называете Bucket, не является тем, что обычно называется Bucket в хеш-таблице. Обычно Bucket - это набор записей с одинаковым размером% хэша. Кажется, вы используете его для того, что обычно называется записью. Связанный список не по сегментам, а по записям.
 – 
unbeli
1 Мар 2013 в 13:36
13
Я использую внутреннюю терминологию PHP. Bucket являются частью двусвязного списка для хеш-коллизий, а также частью двусвязного списка для порядка;)
 – 
NikiC
1 Мар 2013 в 18:47
5
Отличный ответ. Я думаю, вы имели в виду iterate($outerArr);, а не iterate($arr); где-то.
 – 
lud
31 Мар 2016 в 12:04

В примере 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: быстро или нет?

123
Peter Mortensen 15 Апр 2014 в 15:10
Кажется, вы правы, я привел пример, демонстрирующий, что: codepad.org/OCjtvu8r одно отличие от вашего примера - это не копирует, если вы меняете значение, только если меняете ключи.
 – 
zb'
8 Апр 2012 в 01:35
1
Это действительно объясняет все поведение, показанное выше, и это можно хорошо проиллюстрировать, вызвав each() в конце первого тестового примера, где мы видим, что указатель исходного массива указывает на второй элемент, поскольку массив был изменен во время первой итерации. Это также, кажется, демонстрирует, что foreach перемещает указатель массива перед выполнением блока кода цикла, чего я не ожидал - я думал, что он сделает это в конце. Большое спасибо, это хорошо меня проясняет.
 – 
DaveRandom
8 Апр 2012 в 19:59

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

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

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

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

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

В следующем примере показано, что each () и 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
54
Amin.MasterkinG 24 Апр 2019 в 00:57
2
Ваш ответ не совсем правильный. foreach работает с потенциальной копией массива, но не создает фактическую копию, если она не нужна.
 – 
linepogl
8 Апр 2012 в 10:34
Вы хотели бы продемонстрировать, как и когда эта потенциальная копия создается с помощью кода? Мой код демонстрирует, что foreach копирует массив в 100% случаев. Я очень хочу знать. Спасибо за ваши комментарии
 – 
sakhunzai
8 Апр 2012 в 20:05
Копирование массива стоит дорого. Попробуйте подсчитать время, необходимое для перебора массива из 100000 элементов, используя for или foreach. Вы не увидите значительной разницы между ними, потому что фактического копирования не происходит.
 – 
linepogl
9 Апр 2012 в 22:46
Тогда я бы предположил, что есть SHARED data storage зарезервировано до или до copy-on-write, но (из моего фрагмента кода) очевидно, что всегда будет ДВА набора SENTINEL variables по одному для original array и другие для foreach. Спасибо, что имеет смысл
 – 
sakhunzai
9 Апр 2012 в 23:44
1
Да, это «предполагаемая» копия, то есть «потенциальная» копия. Она не защищена, как вы предложили
 – 
sakhunzai
16 Апр 2014 в 10:21

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

Чтобы обновить этот ответ, поскольку он приобрел некоторую популярность: этот ответ больше не применяется с PHP 7. Как объяснено в "Обратно несовместимые изменения", в PHP 7 foreach работает с копией массива, поэтому любые изменения самого массива не отражаются в цикле 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?, где объясняется внутреннее устройство. это поведение.

39
dkasipovic 1 Июн 2018 в 15:23
7
Хорошо, вы прочитали остальную часть ответа? Совершенно логично, что foreach решает, будет ли он зацикливаться в другой раз перед даже запуском кода в нем.
 – 
dkasipovic
15 Апр 2014 в 12:49
2
Нет, массив изменен, но «слишком поздно», поскольку foreach уже «думает», что он находится в последнем элементе (который находится в начале итерации) и больше не будет зацикливаться. Во втором примере он не находится в последнем элементе в начале итерации и вычисляется снова в начале следующей итерации. Я пытаюсь подготовить тестовый пример.
 – 
dkasipovic
15 Апр 2014 в 12:55
1
Посмотрите на lxr.php.net/xref/PHP_TRUNK/Zend/zend_vm_def. h # 4509 При итерации всегда устанавливается на следующий указатель. Итак, когда он достигнет последней итерации, он будет помечен как завершенный (с помощью указателя NULL). Когда вы затем добавляете ключ на последней итерации, foreach этого не заметит.
 – 
bwoebi
15 Апр 2014 в 13:21
1
Нет. Здесь нет полного и ясного объяснения (по крайней мере, на данный момент - может быть, я ошибаюсь)
 – 
Alma Do
15 Апр 2014 в 13:25
4
На самом деле кажется, что @AlmaDo неправильно понимает свою логику ... Ваш ответ хорош.
 – 
bwoebi
15 Апр 2014 в 13:36

Согласно документации, предоставленной руководством по 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; как передача по ссылке.

16
Amal Murali 15 Апр 2014 в 13:35

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

14
Hrvoje Antunović 21 Апр 2017 в 11:44

Цикл PHP foreach можно использовать с 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

8
Pranav Rana 13 Ноя 2017 в 17:08
1
Object public variables неверно или, в лучшем случае, вводит в заблуждение. Вы не можете использовать объект в массиве без правильного интерфейса (например, Traversible), и когда вы делаете foreach((array)$obj ..., вы фактически работаете с простым массивом, а не с объектом.
 – 
Christian
27 Дек 2017 в 00:35