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

1586

Позвольте мне префикс этого, сказав, что я знаю, что 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 */

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

Если мы посмотрим в manual, мы найдем это утверждение:

При первом запуске foreach внутренний указатель массива автоматически reset к первому элементу массива.

Правильно... это, по-видимому, предполагает, что 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)
*/

Таким образом, несмотря на то, что мы не работаем напрямую с исходным массивом, мы работаем напрямую с указателем исходного массива - тот факт, что указатель находится в конце массива в конце цикла, показывает это. Кроме того, это не может быть правдой - если бы это было так, то test case 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, может повлиять на результат цикла?
  • 4
    @DaveRandom Есть тег php-internals, с которым это, вероятно, должно пойти, но я оставлю вам решать, какой из остальных 5 тегов заменить.
  • 0
    попробуйте также unset($array[$key + 1]);
Показать ещё 16 комментариев
Теги:
loops
foreach
iteration
php-internals

7 ответов

1334
Лучший ответ

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) $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 really
// the same array in both loops and not a copy
foreach ($arr as &$v1) {
    foreach ($arr as &$v) {
        // ...
    }
}

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

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

IAP - видимая функция массива (отображается через current семейство функций), так как такие изменения в IAP считаются модификациями в семантике copy-on-write. Это, к сожалению, означает, что 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 в первую очередь).

(*) Инкремент пересчета здесь звучит безобидно, но нарушает семантику copy-on-write (COW): это означает, что мы собираемся изменить IAP массива refcount = 2, в то время как COW диктует, что изменения могут быть выполнены только при пересчете = 1. Это нарушение приводит к изменению поведения пользователя (в то время как COW обычно прозрачен), поскольку изменение IAP в итерированном массиве будет наблюдаемым - но только до первой модификации, отличной от IAP в массиве. Вместо этого тремя "действительными" параметрами были бы: а) чтобы всегда дублировать, b) чтобы не увеличивать коэффициент пересчета и, таким образом, позволяя произвольно модифицировать итерированный массив в цикле или в) вообще не использовать 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. Вот почему примеры кода, отображающие модификацию во время итерации, всегда будут отменять следующий элемент, а не текущий.

Примеры: ваши тестовые примеры

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

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

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

  • В тестовом примере 3 снова массив не дублируется, поэтому foreach будет изменять IAP переменной $array. В конце итерации МАП является 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 (на самом деле: prefer-ref), хотя он не модифицирует массив. Это должно быть для того, чтобы играть хорошо со всеми другими функциями, такими как next которые все по-ref. Прохождение байтов означает, что массив должен быть разделен, и поэтому $array и $array foreach будут разными. Причина, по которой вы получаете 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 a is_ref = 1, когда он повторяется по ссылке, поэтому в основном вы имеете ту же ситуацию, что и выше.

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

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

Здесь refcount $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 удален. Вероятно, неожиданным является то, что внешний цикл останавливается после первого элемента. Почему это?

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

Другим следствием HashPointer резервного копирования + восстановления 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', и распределитель '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: если элемент массива удален, все итераторы хеш-таблицы, указывающие на этот элемент, будут переходить к следующему элементу.

Это означает, что Еогеасп больше не будет использовать 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 сохраняют один и тот же результат: итерация массива по-прежнему всегда работает с исходными элементами. (В этом случае даже пересчет и поведение дублирования точно совпадают между 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 переместился прямо к новому элементу, потому что он "выглядел" так же, как и элемент remove (из-за встречного хэша и указателя). Поскольку мы больше не полагаемся на хэш элементов для чего-либо, это уже не проблема.

  • 0
    + Ницца .. это не решает, почему он работает по-другому с функциями 3v4l.org/1aUpd
  • 4
    @ Баба. Передача его функции аналогична выполнению $foo = $array перед циклом;)
Показать ещё 12 комментариев
102

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

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

Вот пример:

$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: быстрый или нет?

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

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

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

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

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

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

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

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

list ($ key2, $val2) = each ($ array);
echo  "each() Оригинал (внешний): $key2 = > $val2 < br/>";

foreach ($ array as $key = > $val) {   echo "foreach: $key = > $val < br/>";
   list ($ key2, $val2) = each ($ array);   echo "each() Оригинал (внутри): $key2 = > $val2 < br/>";
   эхо "-------- Итерация -------- < br/>";   if ($ key == 3) {       echo "Сброс исходного указателя массива < br/>";       reset ($ массив);   }
}

list ($ key2, $val2) = each ($ array);
echo  "each() Оригинал (внешний): $key2 = > $val2 < br/>";
Код>

Вывод:

  each() Оригинал (внешний): 0 = > 1
foreach: 0 = > 1
каждый() Оригинал (внутри): 1 = > 2
-------- -------- Итерация
foreach: 1 = > 2
каждый() Оригинал (внутри): 2 = > 3
-------- -------- Итерация
foreach: 2 = > 3
каждый() Оригинал (внутри): 3 = > 4
-------- -------- Итерация
foreach: 3 = > 4
каждый() Оригинал (внутри): 4 = > 5
-------- -------- Итерация
Сброс исходного указателя массива
foreach: 4 = > 5
каждый() Оригинал (внутри): 0 = > 1
-------- -------- Итерация
each() Оригинал (внешний): 1 = > 2
Код>
  • 2
    Ваш ответ не совсем правильный. foreach работает с потенциальной копией массива, но не создает фактическую копию, если в этом нет необходимости.
  • 0
    Вы хотели бы продемонстрировать, как и когда эта потенциальная копия создается с помощью кода? Мой код демонстрирует, что foreach копирует массив 100% времени. Я хочу знать. Спасибо за ваши комментарии
Показать ещё 4 комментария
23

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

Чтобы обновить этот ответ, поскольку он приобрел некоторую популярность: этот ответ больше не применяется с PHP 7. Как поясняется в " Отказоустойчивых изменениях ", в PHP 7 foreach работает с копией массива, поэтому любые изменения в самом массиве не отражаются на петле foreach. Подробнее по ссылке.

Объяснение (цитата из php.net):

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

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

  • 7
    Хорошо, вы прочитали остальную часть ответа? Совершенно понятно, что foreach решает, будет ли он зациклен в другой раз, прежде чем он даже выполнит код в нем.
  • 2
    Нет, массив изменен, но «слишком поздно», поскольку foreach уже «думает», что он находится в последнем элементе (который находится в начале итерации) и больше не будет зацикливаться. Где во втором примере, это не последний элемент в начале итерации и снова вычисляется в начале следующей итерации. Я пытаюсь подготовить контрольный пример.
Показать ещё 9 комментариев
8

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

5

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

$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);

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

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

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

4

PHP foreach loop может использоваться с 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 ... вы фактически работаете с простым массивом, а не объектом больше.

Ещё вопросы

Сообщество Overcoder
Наверх
Меню