Как работает 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 */ 

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

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

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

Related of "Как работает PHP 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 не будет создана для комментариев Notes / User .

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

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

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

Detailed explanation can be read at How does PHP 'foreach' actually work? which explains the internals behind this behaviour.

As per the documentation provided by PHP manual.

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).

So as per your first example:

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

$array have only single element, so as per the foreach execution, 1 assign to $v and it don't have any other element to move pointer

But in your second example:

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

$array have two element, so now $array evaluate the zero indices and move the pointer by one. For first iteration of loop, added $array['baz']=3; as pass by reference.

PHP foreach loop can be used with Indexed arrays , Associative arrays and Object public variables .

In foreach loop, the first thing php does is that it creates a copy of the array which is to be iterated over. PHP then iterates over this new copy of the array rather than the original one. This is demonstrated in the below example:

 <?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). 

Besides this, php does allow to use iterated values as a reference to the original array value as well. This is demonstrated below:

 <?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: It does not allow original array indexes to be used as references .

Source: http://dwellupper.io/post/47/understanding-php-foreach-loop-with-examples

Great question, because many developers, even experienced ones, are confused by the way PHP handles arrays in foreach loops. In the standard foreach loop, PHP makes a copy of the array that is used in the loop. The copy is discarded immediately after the loop finishes. This is transparent in the operation of a simple foreach loop. Например:

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

Эти результаты:

 apple banana coconut 

So the copy is created but the developer doesn't notice, because the original array isn't referenced within the loop or after the loop finishes. However, when you attempt to modify the items in a loop, you find that they are unmodified when you finish:

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

Эти результаты:

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

Any changes from the original can't be notices, actually there are no changes from the original, even though you clearly assigned a value to $item. This is because you are operating on $item as it appears in the copy of $set being worked on. You can override this by grabbing $item by reference, like so:

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

Эти результаты:

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

So it is evident and observable, when $item is operated on by-reference, the changes made to $item are made to the members of the original $set. Using $item by reference also prevents PHP from creating the array copy. To test this, first we'll show a quick script demonstrating the copy:

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

As it is shown in the example, PHP copied $set and used it to loop over, but when $set was used inside the loop, PHP added the variables to the original array, not the copied array. Basically, PHP is only using the copied array for the execution of the loop and the assignment of $item. Because of this, the loop above only executes 3 times, and each time it appends another value to the end of the original $set, leaving the original $set with 6 elements, but never entering an infinite loop.

However, what if we had used $item by reference, as I mentioned before? A single character added to the above test:

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

Results in an infinite loop. Note this actually is an infinite loop, you'll have to either kill the script yourself or wait for your OS to run out of memory. I added the following line to my script so PHP would run out of memory very quickly, I suggest you do the same if you're going to be running these infinite loop tests:

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

So in this previous example with the infinite loop, we see the reason why PHP was written to create a copy of the array to loop over. When a copy is created and used only by the structure of the loop construct itself, the array stays static throughout the execution of the loop, so you'll never run into issues.