Как на самом деле работает 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)
*/

Итак, несмотря на то, что мы не работаем непосредственно с исходным массивом, мы работаем непосредственно с указателем исходного массива-тот факт, что указатель находится в конце массива в конце цикла, показывает это. Но это не может быть правдой - если так,то ... --24-->тестовый пример 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() et al.) во время foreach может повлиять на исход цикла?

7 ответов


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

  • массивы
  • нормальные объекты
  • Traversable объекты

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

итерация массивов и простых объектов значительно сложнее. Прежде всего, следует отметить, что в PHP "массивы" - это действительно упорядоченные словари, и они будут проходить в соответствии с этим порядком (который соответствует порядку вставки, если вы не использовали что-то вроде sort). Это противопоставляется итерации естественным порядок ключей (как часто работают списки на других языках) или вообще не имеет определенного порядка (как часто работают словари на других языках).

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

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

  • если вы повторите ссылка с помощью foreach ($arr as &$v) затем $arr превращается в ссылку, и вы можете изменить ее во время итерации.
  • в PHP 5 то же самое применяется, даже если вы повторяете по значению, но массив был ссылкой заранее: $ref =& $arr; foreach ($ref as $v)
  • объекты имеют семантику передачи по дескриптору, что для практических целей означает, что они ведут себя как ссылки. Таким образом, объекты всегда могут быть изменены во время итерации.

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

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

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

PHP 5

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

дублирование массива

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

  1. массив не является ссылкой (is_ref=0). Если это ссылка, то изменения в ней являются должно для распространения, поэтому он не должен дублироваться.
  2. массив имеет refcount>1. Если refcount равен 1, то массив не является общим, и мы можем изменить его напрямую.

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

порядок продвижения позиции

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

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

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

примеры: тесты

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

поведение из ваших тестовых случаев просто объяснить на данный момент:

  • в тестовых случаях 1 и 2 $array начинается с refcount=1, поэтому он не будет дублироваться foreach: увеличивается только refcount. Когда тело цикла впоследствии изменяет массив (который имеет refcount=2 в этой точке), дублирование произойдет в этой точке. Foreach продолжит работу над неизмененной копией $array.

  • в тестовом случае 3, еще раз массив не дублируется, поэтому foreach будет изменять IAP $array переменной. В конце итерации IAP равен NULL (что означает итерацию), которая each указывает на возвращение false.

  • в тестовых случаях 4 и 5 Оба each и reset по-справочных функций. The $array есть refcount=2, когда он передал им, поэтому он должен быть дублирован. Как таковой foreach будет работать над отдельным массивом снова.

примеры: последствия current в foreach

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

замена итерированного объекта во время цикла

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

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

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

это означает, что foreach больше не будет использовать IAP на всех. Цикл foreach не будет абсолютно никакого влияния на результаты current() etc. и его собственное поведение никогда не будет зависеть от таких функций, как reset() etc.

дублирование массива

еще одно важное изменение между PHP 5 и PHP 7 связано с дублированием массива. Теперь, когда IAP больше не используется, итерация массива по значению будет выполнять только увеличение refcount (вместо дублирования массива) во всех случаях. Если массив изменяется во время цикла foreach, в этот момент произойдет дублирование (согласно copy-on-write), и 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. этот особый случай-ушли:-значение итерации массива будет всегда продолжайте работать над оригинальными элементами, игнорируя любые изменения во время цикла.

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

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

это отражает семантику объектов по дескриптору (т. е. они ведут себя как ссылки даже в контекстах по значению).

примеры

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

  • тестовые примеры 1 и 2 сохраняют один и тот же вывод: массив значений итерация всегда работает над исходными элементами. (В этом случае даже поведение пересчета и дублирования точно такое же между PHP 5 и PHP 7).

  • тестовый случай 3 изменения: Foreach больше не использует IAP, поэтому each() не влияет на цикл. Он будет иметь тот же результат до и после.

  • тестовые случаи 4 и 5 остаются неизменными:each() и reset() будет дублировать массив перед изменением IAP, в то время как foreach все еще использует исходный массив. (Не то чтобы изменение IAP имело значение, даже если массив был общим.)

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


в Примере 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: быстрый или нет?


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

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

b) что вызывает a разведанные копия? Поисковая копия создается на основе политики 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 7

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

объяснение (цитата из php.net):

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

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

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

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


Отличный вопрос, потому что многие разработчики, даже опытные, путаются в том, как 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 by reference также предотвращает создание 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
)

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


PHP foreach loop можно использовать с 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