элемент - циклы php




Как работает PHP foreach? (5)

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

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

В примере 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 Ternary Operator: Fast или нет?


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

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

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

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

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

В следующем примере показано, что каждый () и 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

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


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


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

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

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

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

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

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





php-internals