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

2019

Позвольте мне поставить префикс этого, сказав, что я знаю, что 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могли повлиять на исход цикла?
DaveRandom
источник
5
@DaveRandom Есть тег php-internals, с которым это, вероятно, должно пойти, но я оставлю вам решать, какой из остальных 5 тегов заменить.
Майкл Берковски
5
выглядит как COW, без дескриптора удаления
zb '
149
Сначала я подумал, черт возьми, еще один вопрос новичка. Прочитайте документы ... хм, явно неопределенное поведение «. Затем я читаю полный вопрос и должен сказать: мне это нравится. Вы приложили немало усилий и написали все тестовые случаи. пс. Тестовые примеры 4 и 5 одинаковы?
knittl
21
Просто мысль о том, почему имеет смысл касаться указателя массива: PHP необходимо сбросить и переместить внутренний указатель массива исходного массива вместе с копией, потому что пользователь может запросить ссылку на текущее значение ( foreach ($array as &$value)) - PHP должен знать текущую позицию в исходном массиве, даже если он фактически перебирает копию.
Нико
4
@Sean: ИМХО, документация PHP довольно плохо описывает нюансы основных функций языка. Но это, возможно, потому, что в язык
запечено

Ответы:

1660

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, есть дополнительное осложнение: существует только один 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создаст резервную копию указателя на текущий элемент и его хэш в per-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будет продублировано, чтобы предотвратить $arrутечку изменений IAP $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реализации, и мы можем перейти к обсуждению некоторых примеров.

Поведение ваших тестовых примеров просто объяснить в этой точке:

  • В тестовых $arrayпримерах 1 и 2 начинается с 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()это функция by-ref (на самом деле :fer-ref), даже если она не модифицирует массив. Это должно быть для того, чтобы хорошо играть со всеми другими функциями, такими как next, все-by-ref. Передача по ссылке подразумевает, что массив должен быть отделен и, следовательно, $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, поэтому массив не копируется (как и выше). Но теперь, когда это ссылка, массив больше не должен дублироваться при передаче в current()функцию by-ref . Таким образом current()и foreachработают на одном массиве. Тем не менее, вы по-прежнему видите поведение «один за другим» из-за того, как foreachпродвигается указатель.

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

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

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

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

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

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

Примеры: модификация во время итерации

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

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

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 и хэш копируются в a 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

Hashtable итераторы

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

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

Это означает , что foreachбольше не будет использовать ИПД на всех . 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
источник
4
@ Баба. Передача его в функцию аналогична $foo = $array
передаче
32
Для тех из вас, кто не знает, что такое zval, обратитесь к блогу
shu zOMG chen
1
Небольшое исправление: то, что вы называете Bucket, не то, что обычно называется Bucket в хеш-таблице. Обычно Bucket - это набор записей с одинаковым размером хеша%. Вы, кажется, используете его для того, что обычно называется записью. Связанный список не на ведрах, но на записях.
Unbeli
12
@unbeli Я использую терминологию, используемую внутри PHP. Они Bucketявляются частью двусвязного списка для коллизий хэшей, а также частью двусвязного списка для порядка;)
NikiC
4
Отличный ответ. Я думаю, что вы имели в виду, iterate($outerArr);а не iterate($arr);где-то.
niahoo
116

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

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

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

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

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

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

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

В следующем примере показано , что каждый () и сброс () не влияет на 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
sakhunzai
источник
2
Ваш ответ не совсем правильный. foreachработает с потенциальной копией массива, но не создает фактическую копию, если в этом нет необходимости.
linepogl
Вы хотели бы продемонстрировать, как и когда эта потенциальная копия создается с помощью кода? Мой код демонстрирует, что foreachкопирует массив 100% времени. Я хочу знать. Спасибо за ваши комментарии
sakhunzai
Копирование массива стоит дорого. Попытка подсчета времени, которое требуется для выполнения итерации массив с 100000 элементов с использованием либо forили foreach. Вы не увидите никакой существенной разницы между ними, потому что фактическая копия не имеет места.
linepogl
Тогда я бы предположил, что SHARED data storageзарезервировано до или без copy-on-write, но (из моего фрагмента кода) очевидно, что всегда будет ДВА набора из SENTINEL variablesодного для original arrayи другого для foreach. Спасибо, что имеет смысл
Сахунзай
1
да, это «предполагаемая» копия, то есть «потенциальная» копия. Она не защищена, как вы предложили
sakhunzai
33

ПРИМЕЧАНИЕ ДЛЯ 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'? который объясняет внутренности этого поведения.

dkasipovic
источник
7
Хорошо, вы прочитали остальную часть ответа? Совершенно понятно, что foreach решает, будет ли он зациклен в другой раз, прежде чем он даже выполнит код в нем.
dkasipovic
2
Нет, массив изменен, но "слишком поздно", поскольку foreach уже "думает", что он находится в последнем элементе (который находится в начале итерации) и больше не будет зацикливаться. Где во втором примере, это не последний элемент в начале итерации и вычисляется снова в начале следующей итерации. Я пытаюсь подготовить контрольный пример.
dkasipovic
1
@AlmaDo Посмотрите на lxr.php.net/xref/PHP_TRUNK/Zend/zend_vm_def.h#4509 Он всегда установлен на следующий указатель, когда он повторяется. Поэтому, когда он достигнет последней итерации, он будет помечен как завершенный (через указатель NULL). Когда вы добавите ключ в последней итерации, foreach не заметит этого.
bwoebi
1
@DKasipovic нет. Там нет полного и четкого объяснения (по крайней мере, на данный момент - может быть, я ошибаюсь)
Алма До
4
На самом деле кажется, что @AlmaDo имеет недостаток в понимании его собственной логики ... Ваш ответ в порядке.
bwoebi
15

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

user3535130
источник
13

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

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

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

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

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

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

Хрвое Антунович
источник
7

Цикл 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

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