PHP7 - изменения в foreach

Категория: / DEV Блог / PHP (LAMP)
В PHP5 присутствует неоднозначность в работе foreach, которая возникает из-за манипуляции с внутренним указателем массива или модификации элементов массива, который перебирается в foreach.

Поведение foreach может зависить от ссылочного счетчика или типа итерируемого объекта - является он значением или ссылкой.

Вот несколько примеров foreach

1. Неработающий current()

$a = [1,2,3]; foreach($a as $v) {echo $v . " - " . current($a) . "\n";}
 
1 - 2
2 - 2
3 - 2
 
$a = [1,2,3]; $b = &$a; foreach($a as $v) {echo $v . " - " . current($a) . "\n";}
 
1 - 2
2 - 3
3 -
 
 
$a = [1,2,3]; $b = $a; foreach($a as $v) {echo $v . " - " . current($a) . "\n";}
 
1 - 1
2 - 1
3 - 1


2. unset() может исключить элемент из перебора, а может и нет

$a = [1,2,3]; foreach($a as $v) {echo "$v\n"; unset($a[1]);}
 
1
2
3
 
$a = [1,2,3]; $b = &$a; foreach($a as $v) {echo "$v\n"; unset($a[1]);}
 
1
3


В PHP7 реализован более рациональный подход.

Первое, foreach может итерировать:

- по значению - foreach ($a as $v)
- по ссылке - foreach ($a as &$v)

Второе, foreach может перебирать

- массив $x = [1,2,3]; foreach ($x as $v)
- простой объект - $x = new SomeClass(); foreach ($x as $v)
- итератор - $x = new SomeIteratorClass(); foreach ($x as $v)

Итерация массива по значению

Никогда не использует и меняет внутренний указатель массива.
Не делает копию массива, а лочит оригинальный массив (реф.счетчик++)

$a = [1,2,3]; foreach($a as $v) {echo $v . " - " . current($a) . "\n";
 
// до PHP 7
1 - 2
2 - 2
3 - 2
 
// PHP 7+
1 - 1
2 - 1
3 - 1


Модификации оригинального массива игнорируются

$a = [1,2,3]; $b = &$a; foreach($a as $v) {echo "$v\n"; unset($a[1]);}
 
// до PHP 7 result
1
3
 
// PHP 7+
1
2
3


Итерация массива по ссылке

В основном повторяет поведение PHP5.

Изменяет внутренний указатель (передвигает его на следующий элемент)

$a = [1,2,3]; foreach($a as &$v) {echo $v . " - " . current($a) . "\n"; }
 
// до PHP 7
1 - 2
2 - 3
3 -
 
// PHP 7+
1 - 1
2 - 1
3 - 1
 
в RFC результаты PHP7 отличаются
1 - 2
2 - 3
3 -


@wtf: и где он его изменяет, нужно уточнение?

Модификация внутреннего указателя функциями next(), reset(), end() не отражаются на указателе foreach,
при следующей итерации foreach восстановит свой указатель (как в PHP5)

$a = [1,2,3,4]; foreach($a as &$v) {echo "$v - "; next($a); var_dump(current($a));}
 
// PHP 5.4 - 7+
1 - int(3)
2 - int(4)
3 - bool(false)
4 - bool(false)


Удаление следующего элемента (указателя foreach) удалит его из перебора

$a = [1,2,3]; foreach($a as &$v) {echo "$v\n"; unset($a[1]);}
 
// PHP 5.4 - 7+
1
3


Добавление новых элементов после указателя, добавит их в перебор

$a = [1,2]; foreach($a as &$v) {echo "$v\n"; $a[2]=3;}
 
// PHP 5.4 - 7+
1
2
3


Добавление новых элементов после указателя (когда мы находимся в конце массива), все равно добавит их в перебор

$a = [1,2]; $b = [3,4]; next($b); foreach($a as &$v) {echo "$v\n"; $a = $b;}
 
// PHP 5.4 - 7+
1
4
 
// hhvm фейлит данный тест с бесконечным циклом


В случае использования нескольких ссылочного foreach по одному и тому же массиву, работают теже правила, независимо друг от друга (не работает в PHP5)

$a = [0, 1, 2, 3];
foreach ($a as &$x) {
        foreach ($a as &$y) {
                echo "$x - $y\n";
                if ($x == 0 && $y == 1) {
                        unset($a[1]);
                        unset($a[2]);
                }
        }
}
 
// PHP5.4
0 - 0
0 - 1
0 - 3
 
// PHP 7+
0 - 0
0 - 1
0 - 3
3 - 0
3 - 3


Возможно изменение массива, итерируемого в ссылочном foreach используя функции array_pop(), array_push(), array_shift(), array_unshift()
Эти функции сохраняют указатель массива foreach или передвигают его к следующему элементу, если текущий удален (не работает в PHP5)


$a=[1,2,3,4]; foreach($a as &$v) { echo "$v\n"; array_pop($a);}
 
 
// PHP5.4
1
2
1
1
 
// PHP 7+
1
2


Итерация простых объектов по значению

Работает также как итерация массива по ссылке, но используется значение объекта, вместо ссылки, как результат объект может быть изменен, НО не заменен.

Итерация простых объектов по ссылке

Работает также как итерация массива по ссылке


@Ссылки
RFC: Fix "foreach" behavior
Миграция с PHP5.X -> PHP7
Онлайн выполнение PHP кода для тестирования всех версий PHP