Как работает PHP foreach?

Позвольте мне префикс этого, сказав, что я знаю, что 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, может повлиять на результат цикла?
+1824
07 апр. '12 в 19:33
источник поделиться
8 ответов

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. Эта схема в основном своего рода работает, но есть много странного поведения, из которого можно выйти, некоторые из которых я продемонстрирую ниже.

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

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 здесь 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. В конце итерации, МАП является 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, поэтому массив не копируется (как и выше). Но теперь, когда это ссылка, массив больше не нужно дублировать при передаче в функцию 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 */

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

Причиной этого является хак с вложенным циклом, описанный выше: перед выполнением тела цикла текущая позиция 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', и распределитель случайно использует ту же ячейку памяти для хранения элемента. Таким образом, 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 больше не будет использовать IAP вообще. Цикл foreach окажет абсолютно никакого влияния на результаты current() и т.д., И на его собственное поведение никогда не будут влиять такие функции, как reset() и т.д.

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

Другое важное изменение между PHP 5 и PHP 7 связано с дублированием массива. Теперь, когда IAP больше не используется, итерация массива по значению будет делать refcount приращение 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() не влияет на 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 перешел прямо к новому элементу, потому что он "выглядел" так же, как и удаленный элемент (из-за столкновения хеша и указателя). Поскольку мы больше ни на что не полагаемся на хэш элемента, это больше не проблема.

+1514
13 февр. '13 в 13:21
источник

Связанные вопросы


Похожие вопросы

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

+106
07 апр. '12 в 20:43
источник

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

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

б) Что вызывает предполагаемую копию? Предполагаемая копия создается на основе политики 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() итератора 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
+42
07 апр. '12 в 21:03
источник

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

+29
15 апр. '14 в 8:46
источник

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

+14
15 апр. '14 в 9:32
источник

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

+11
21 апр. '17 в 8: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

+7
13 нояб. '17 в 14:08
источник

пользователь это;

$models = array();
$q="select * from table";
$getmodels = mysql_query($q);
while($res = mysql_fetch_array($getmodels)) { 
$models[$res['id']] = $res['title'];
}
foreach($models as  $key => $value){
echo $key;
echo '<br>';
echo $value;
}

Я использую этот код в ссылке

0
14 мая '19 в 16:29
источник

Посмотрите другие вопросы по меткам или Задайте вопрос