Позвольте мне префикс этого, сказав, что я знаю, что такое 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 может повлиять на результат цикла?
2131
DaveRandom 7 Апр 2012 в 23:33

6 ответов

Цикл PHP foreach можно использовать с Indexed arrays, Associative arrays и Object public variables.

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

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

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

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

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

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

8
Pranav Rana 13 Ноя 2017 в 14:08

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

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

Это выводит:

apple
banana
coconut

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

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

print_r($set);

Это выводит:

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

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

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

Это выводит:

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

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

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

Это выводит:

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

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

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

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

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

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

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

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

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

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

Итак, согласно вашему первому примеру:

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

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

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

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

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

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

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

38
dkasipovic 1 Июн 2018 в 12:23

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

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

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

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

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

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

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

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

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

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

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

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

Вывод:

each() Original (outside): 0 => 1
foreach: 0 => 1
each() Original(inside): 1 => 2
--------Iteration--------
foreach: 1 => 2
each() Original(inside): 2 => 3
--------Iteration--------
foreach: 2 => 3
each() Original(inside): 3 => 4
--------Iteration--------
foreach: 3 => 4
each() Original(inside): 4 => 5
--------Iteration--------
Resetting original array pointer
foreach: 4 => 5
each() Original(inside): 0=>1
--------Iteration--------
each() Original (outside): 1 => 2
52
Amin.MasterkinG 23 Апр 2019 в 21:57

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

119
Peter Mortensen 15 Апр 2014 в 11:10