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

1798

Позвольте мне префикс этого, сказав, что я знаю, чтоforeach есть, делает и как его использовать. Этот вопрос касается того, как он работает под капотом, и я не хочу никаких ответов в духе «вот как вы зацикливаете массив с помощьюforeach& Quot ;.

Долгое время я предполагал, чтоforeach работал с самим массивом. Потом я нашел много ссылок на тот факт, что он работает сcopy массива, и с тех пор я предположил, что это конец истории. Но я недавно вступил в дискуссию по этому вопросу, и после небольшого эксперимента обнаружил, что на самом деле это не на 100% верно.

Позвольте мне показать, что я имею в виду. Для следующих тестовых случаев мы будем работать со следующим массивом:

<code>$array = array(1, 2, 3, 4, 5);
</code>

Тестовый пример 1:

<code>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 */
</code>

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

Контрольный пример 2:

<code>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 */
</code>

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

Если мы посмотрим вруководствомы находим это утверждение:

When foreach first starts executing, the internal array pointer is automatically reset to the first element of the array.

Правильно ... это говорит о том, чтоforeach опирается на указатель массива исходного массива. Но мы только что доказали, что мыnot working with the source array, право? Ну, не совсем.

Контрольный пример 3:

<code>// 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)
*/
</code>

Таким образом, несмотря на то, что мы не работаем напрямую с исходным массивом, мы работаем напрямую с указателем на исходный массив - тот факт, что указатель находится в конце массива в конце цикла, показывает это. За исключением того, что это не может быть правдой - если это было так, токонтрольный пример 1 будет цикл навсегда.

В руководстве по PHP также говорится:

As foreach relies on the internal array pointer changing it within the loop may lead to unexpected behavior.

Что ж, давайте выясним, что это за "неожиданное поведение". есть (технически, любое поведение является неожиданным, так как я больше не знаю, чего ожидать).

Контрольный пример 4:

<code>foreach ($array as $key => $item) {
  echo "$item\n";
  each($array);
}

/* Output: 1 2 3 4 5 */
</code>

Контрольный пример 5:

<code>foreach ($array as $key => $item) {
  echo "$item\n";
  reset($array);
}

/* Output: 1 2 3 4 5 */
</code>

... ничего неожиданного там, на самом деле, кажется, что он поддерживает "копию источника" теория.

The Question

Что здесь происходит? Мой C-fu не достаточно хорош для того, чтобы я мог извлечь правильное заключение, просто взглянув на исходный код PHP, я был бы признателен, если бы кто-то смог перевести его на английский для меня.

Мне кажется, чтоforeach работает сcopy массива, но устанавливает указатель массива исходного массива в конец массива после цикла.

Is this correct and the whole story? If not, what is it really doing? Is there any situation where using functions that adjust the array pointer (each(), reset() et al.) during a foreach could affect the outcome of the loop?
Сначала я подумал, черт возьми, еще один вопрос новичка. Прочитайте документы & # x2026; хм, явно неопределенное поведение & # xAB ;. Затем я читаю полный вопрос и должен сказать: мне это нравится. Вы приложили немало усилий и написали все тестовые случаи. пс. Тестовые примеры 4 и 5 одинаковы? knittl
Просто мысль о том, почему имеет смысл касаться указателя массива: PHP необходимо сбросить и переместить внутренний указатель массива исходного массива вместе с копией, потому что пользователь может запросить ссылку на текущее значение (foreach ($array as &$value)) - PHP должен знать текущую позицию в исходном массиве, даже если он фактически перебирает копию. Niko
@Sean: ИМХО, документация PHP довольно плохо описывает нюансы основных функций языка. Но это, возможно, потому что так много специальных случаев запечено в языке ... Oliver Charlesworth
@DaveRandomphp-internals с этим тегом, вероятно, следует идти, но я оставлю вам решать, какой из других 5 тегов заменить. Michael Berkowski
выглядит как COW, без ручки удаления zb'

Ваш Ответ

7   ответов
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

Note: Не позволяетoriginal array indexes использоваться какreferences.

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

Object public variablesError: User Rate Limit Exceededforeach((array)$obj ...Error: User Rate Limit Exceeded
42

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

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

б) что вызываетprospected copy? Prospected copy is created based on the policy of copy-on-writeто есть всякий раз, когда     массив, переданный в foreach (), изменен, создан клон исходного массива.

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

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

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

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

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

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

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

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

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

Output:

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
Error: User Rate Limit Exceeded
Error: User Rate Limit ExceededforeachError: User Rate Limit Exceeded
Error: User Rate Limit ExceededSHARED data storageError: User Rate Limit Exceededcopy-on-writeError: User Rate Limit ExceededSENTINEL variablesError: User Rate Limit Exceededoriginal arrayError: User Rate Limit ExceededforeachError: User Rate Limit Exceeded
Error: User Rate Limit ExceededforError: User Rate Limit ExceededforeachError: User Rate Limit Exceeded
Error: User Rate Limit ExceededforeachError: User Rate Limit Exceeded
11

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

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

Это выводит:

apple
banana
coconut

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

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

print_r($set);

Это выводит:

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

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

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

Это выводит:

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

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

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

Это выводит:

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

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

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

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

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

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

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

106

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

Error: User Rate Limit Exceededeach()Error: User Rate Limit Exceededwe seeError: User Rate Limit ExceededforeachError: User Rate Limit Exceeded DaveRandom
Error: User Rate Limit Exceededcodepad.org/OCjtvu8rError: User Rate Limit Exceeded
13

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

On each iteration, the value of the current element is assigned to $v and the internal
array pointer is advanced by one (so on the next iteration, you'll be looking at the next element).

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

$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; как передать по ссылке.

29

NOTE FOR PHP 7

Чтобы обновить этот ответ, поскольку он приобрел некоторую популярность: этот ответ больше не применяется с PHP 7. Как объяснено в & quot;Обратно несовместимые изменения& quot ;, в PHP 7 foreach работает с копией массива, поэтому любые изменения самого массива не отражаются в цикле foreach. Подробнее по ссылке.

Explanation (quote from php.net):

The first form loops over the array given by array_expression. On each iteration, the value of the current element is assigned to $value and the internal array pointer is advanced by one (so on the next iteration, you'll be looking at the next element).

Итак, в вашем первом примере у вас есть только один элемент в массиве, и когда указатель перемещается, следующий элемент не существует, поэтому после добавления нового элемента foreach заканчивается, потому что он уже «решен». что это он как последний элемент.

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

Я считаю, что это все следствиеOn each iteration часть объяснения в документации, что, вероятно, означает, чтоforeach делает всю логику, прежде чем он вызывает код в{}.

Test case

Если вы запустите это:

<?
    $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»? на самом деле работа? который объясняет внутренности этого поведения.

Error: User Rate Limit ExceededbeforeError: User Rate Limit Exceeded
Error: User Rate Limit Exceededlxr.php.net/xref/PHP_TRUNK/Zend/zend_vm_def.h#4509Error: User Rate Limit Exceeded
Error: User Rate Limit Exceeded
Error: User Rate Limit Exceededcomplete & clearError: User Rate Limit Exceeded
Error: User Rate Limit Exceeded
1491

foreach поддерживает итерацию по трем различным типам значений:

Далее я попытаюсь объяснить, как итерация работает в разных случаях. Безусловно, самый простой случай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). Это противоречит итерации по естественному порядку ключей (как часто работают списки на других языках) или вообще не имеет определенного порядка (как часто работают словари на других языках).

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

Все идет нормально. Перебор словаря не может быть слишком сложным, верно? Проблемы начинаются, когда вы понимаете, что массив / объект может меняться во время итерации. Это может произойти несколькими способами:

  • If you iterate by reference using foreach ($arr as &$v) then $arr is turned into a reference and you can change it during iteration.
  • In PHP 5 the same applies even if you iterate by value, but the array was a reference beforehand: $ref =& $arr; foreach ($ref as $v)
  • Objects have by-handle passing semantics, which for most practical purposes means that they behave like references. So objects can always be changed during iteration.

Проблема с разрешением модификаций во время итерации - это тот случай, когда элемент, на котором вы сейчас находитесь, удален. Скажем, вы используете указатель, чтобы отслеживать, какой элемент массива вы используете в данный момент. Если этот элемент теперь освобожден, у вас остается висячий указатель (обычно приводящий к segfault).

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

В качестве последнего предварительного замечания следует отметить, что PHP использует подсчет ссылок и копирование при записи для управления памятью. Это означает, что если вы «копируете» значение, вы на самом деле просто повторно используете старое значение и увеличиваете его счетчик ссылок (refcount). Только после того, как вы выполните какую-либо модификацию, будет сделана настоящая копия (называемая «дублирование»). УвидетьВам лгут для более обширного введения по этой теме.

PHP 5

Internal array pointer and HashPointer

Массивы в PHP 5 имеют один выделенный «указатель на внутренний массив». (IAP), который должным образом поддерживает модификации: всякий раз, когда элемент удаляется, будет проверяться, указывает ли IAP на этот элемент. Если это так, вместо этого он продвигается к следующему элементу.

Хотя foreach действительно использует IAP, есть дополнительное осложнение: существует только один IAP, но один массив может быть частью нескольких циклов foreach:

// Using by-ref iteration here to make sure that it's really
// the same array in both loops and not a copy
foreach ($arr as &$v1) {
    foreach ($arr as &$v) {
        // ...
    }
}

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

Array duplication

IAP является видимой особенностью массива (экспонируется черезcurrent семейство функций), так как такие изменения в IAP считаются модификациями в семантике копирования при записи. К сожалению, это означает, что foreach во многих случаях вынужден дублировать массив, который он повторяет. Точные условия:

  1. The array is not a reference (is_ref=0). If it's a reference, then changes to it are supposed to propagate, so it should not be duplicated.
  2. The array has refcount>1. If refcount is 1, then the array is not shared and we're free to modify it directly.

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

Position advancement order

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

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();
}

А именно, указатель массива уже сдвинутbefore тело цикла работает. Это означает, что пока тело цикла работает над элементом$iИАП уже у элемента$i+1, По этой причине примеры кода, показывающие изменение во время итерации, всегда будут сбрасыватьnext элемент, а не текущий.

Examples: Your test cases

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

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

  • In test cases 1 and 2 $array starts off with refcount=1, so it will not be duplicated by foreach: Only the refcount is incremented. When the loop body subsequently modifies the array (which has refcount=2 at that point), the duplication will occur at that point. Foreach will continue working on an unmodified copy of $array.

  • In test case 3, once again the array is not duplicated, thus foreach will be modifying the IAP of the $array variable. At the end of the iteration, the IAP is NULL (meaning iteration has done), which each indicates by returning false.

  • In test cases 4 and 5 both each and reset are by-reference functions. The $array has a refcount=2 when it is passed to them, so it has to be duplicated. As such foreach will be working on a separate array again.

Examples: Effects of current in foreach

Хороший способ показать различные варианты дублирования - это наблюдать за поведениемcurrent() функция внутри цикла foreach. Рассмотрим этот пример:

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

Здесь вы должны знать, чтоcurrent() является функцией by-ref (на самом деле: предпочитаем-ref), даже если она не изменяет массив. Это должно быть для того, чтобы хорошо играть со всеми другими функциями, такими какnext которые все по-реф. Передача по ссылке подразумевает, что массив должен быть отделен и таким образом$array и массив foreach будет другим. Причина, по которой вы получаете2 вместо1 также упоминается выше:foreach продвигает указатель массиваbefore запуск кода пользователя, а не после. Так что, хотя код находится в первом элементе, foreach уже продвинул указатель на второй элемент.

Теперь давайте попробуем небольшую модификацию:

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

Здесь у нас есть случай is_ref = 1, поэтому массив не копируется (как и выше). Но теперь, когда это ссылка, массив больше не должен дублироваться при передаче в by-refcurrent() функция. таким образом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 */

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

Examples: Modification during iteration

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

Substituting the iterated entity during the loop

Последний странный случай, о котором я хотел бы упомянуть, это то, что 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 iterators

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

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

Это означает, что foreach больше не будет использовать IAPat all, Цикл foreach не окажет абсолютно никакого влияния на результатыcurrent() и т.д., и на его собственное поведение никогда не будут влиять такие функции, какreset() и т.п.

Array duplication

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

Это, конечно, не относится к итерации по ссылкам. Если вы выполняете итерацию по ссылке, все изменения будут отражены символом l, oop. Интересно, что то же самое верно для итерации по значению простых объектов:

$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 */

Это отражает семантику отдельных объектов (т. Е. Они ведут себя как ссылки даже в контексте значений).

Examples

Давайте рассмотрим несколько примеров, начиная с ваших тестовых случаев:

  • Test cases 1 and 2 retain the same output: By-value array iteration always keep working on the original elements. (In this case, even refcounting and duplication behavior is exactly the same between PHP 5 and PHP 7).

  • Test case 3 changes: Foreach no longer uses the IAP, so each() is not affected by the loop. It will have the same output before and after.

  • Test cases 4 and 5 stay the same: each() and reset() will duplicate the array before changing the IAP, while foreach still uses the original array. (Not that the IAP change would have mattered, even if the array was shared.)

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

Error: User Rate Limit Exceedediterate($outerArr);Error: User Rate Limit Exceedediterate($arr);Error: User Rate Limit Exceeded
Error: User Rate Limit Exceeded
Error: User Rate Limit ExceededBucketError: User Rate Limit Exceeded
Error: User Rate Limit Exceededblog.golemon.com/2007/01/youre-being-lied-to.html
Error: User Rate Limit Exceeded$foo = $arrayError: User Rate Limit Exceeded

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