Почему не работает цикл foreach

О тонкостях работы foreach в PHP

В недавнем дайджесте интересных ссылок о PHP я обнаружил ссылку на комментарий Никиты Попова на StackOverflow, где он подробно рассказывает о механизме «под капотом» управляющей конструкции foreach.
Поскольку foreach действительно иногда работает более, чем странным образом, я счел полезным сделать перевод этого ответа.

Внимание: этот текст подразумевает наличие базовых знаний о функциональности zval’ов в PHP, в частности вы должны знать что такое refcount и is_ref.
foreach работает с сущностями разных типов: с массивами, с простыми объектами (где перечисляются доступные свойства) и с Traversable-объектами (вернее, объектами, у которых определен внутренний обработчик get_iterator). Здесь мы, в основном, говорим о массивах, но я скажу и об остальных в самом конце.

Прежде чем приступить, пара слов о массивах и их обходе, важная для понимания контекста.

Как работает обход массивов

Массивы в PHP являются упорядоченными хеш-таблицами (элементы хеша объединены в двусвязный список) и foreach обходит массив, следуя указанному порядку.

PHP включает в себя два способа обхода массива:

  • Первый способ — внутренний указатель массива. Этот указатель является частью структуры HashTable и представляет собой просто указатель на текущий элемент хеш-таблицы. Внутренний указатель массива можно безнаказанно изменять, то есть если текущий элемент удалён, внутренний указатель массива будет перемещен на следующий.
  • Второй механизм итерации — внешний указатель массива, под названием HashPosition. Это практически то же самое, что и внутренний указатель массива, но он не является частью HashTable. Этот внешний способ итерации не безопасен к изменениям. Если вы удалите элемент, на который указывает HashPosition, то останетесь с висячим указателем, что приведёт к ошибке сегментации.

Таким образом, внешние указатели массива могут быть использованы только когда вы полностью уверены, что при обходе никакого пользовательского кода выполняться не будет. А такой код может оказаться в самом неожиданном месте, типа обработчика ошибок или деструктора. Вот почему в большинстве случаев PHP приходится использовать внутренний указатель вместо внешнего. Если бы это было иначе, PHP мог бы упасть из-за segmentation fault, как только пользователь начнет делать что-нибудь необычное.

Проблема внутреннего указателя в том, что он является частью HashTable. Так что, когда вы изменяете его, HashTable меняется вместе с ним. И коль скоро обращение к массивам в PHP делается по значению (а не по ссылке), вы вынуждены копировать массив, чтобы в цикле обходить его элементы.

Простой пример, показывающий важность копирования (кстати, не такая большая редкость), это вложенная итерация:

Здесь вы хотите чтобы оба цикла были независимым, а не хитро перебрасывались одним указателем.

Итак, мы дошли до foreach.

Обход массива в foreach

Теперь вы знаете, для чего foreach приходится создавать копию массива, прежде чем обойти его. Но это явно не вся история. Сделает PHP копию или нет, зависит от нескольких факторов:

    Если итерируемый массив является ссылкой, копирования не произойдёт, вместо этого будет выполнен addref:

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

  • Если массив имеет refcount=1, копирование, опять таки, не будет выполнено. refcount=1 означает, что массив не используется в другом месте и foreach может использовать его напрямую. Если refcount больше одного, значит массив разделен с другими переменными и для того чтобы избежать изменения, foreach должен скопировать его (независимо от случая ссылки, описанного выше).
  • Если массив обходится по ссылкам (foreach ($array as &$ref)), то — независимо от функции копирования или не-копирования — массив станет ссылкой.
  • Читайте также:  Как настроить мышку блади j90

    Итак, это первая часть тайны: функция копирования. Вторая часть это то, как текущая итерация выполняется, и она тоже довольно странная. «Обычный» образец итерации, который вы уже знаете (и который часто используется в PHP — отдельно от foreach) выглядит примерно так (псевдокод):

    итерация foreach выглядит немного иначе:

    Отличие в том, что move_forward() выполняется в начале, а не в конце цикла. Таким образом, когда код пользователя использует элемент $i, внутренний указатель массива уже указывает на элемент $i+1.

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

    Последствия для кода

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

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

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

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

    А сейчас вернёмся к вашим примерам:

    Здесь $array имеет refcount=1 до цикла, так что он не будет копирован, но получит addref. Как только вы присвоите значение $array[], zval будет разделен, так что массив, к которому вы добавляете элементы и итерируемый массив будут двумя разными массивами.

    Та же ситуация, что и в первом тесте.

    Снова та же история. Во время цикла foreach, у вас refcount=1 и вы получаете только addref, внутренний указатель $array будет изменён. В конце цикла указатель становится NULL (это означает что итерация закончена). each демонстрирует это, возвращая false.

    Функции each и reset обе обращаются по ссылке. $array имеет refcount=2 когда доходит до них, в результате чего он должен быть разделен. Снова foreach сработает на отдельном массиве.

    Но эти примеры недостаточно убедительны. Поведение начинает быть по настоящему непредсказуемым, когда вы используете current в цикле:

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

    Теперь попробуем сделать небольшое изменение:

    Здесь у нас is_ref=1, так что массив не копирован (так как и выше). Но сейчас когда есть is_ref, массив больше не нужно разделять, передавая по ссылке к current. Теперь current и foreach работают с одним массивом. Вы видите массив сдвинутым на единицу как раз из-за того, как foreach обращается с указателем.

    Читайте также:  Citroen berlingo 2007 не работает спидометр

    То же самое вы увидите, когда будете делать обход массива по ссылкам:

    Здесь самое важное — то, что foreach назначит нашему $array is_ref=1, когда он будет обходить его в цикле по ссылке, так что получится то же, что и выше.

    Еще одна небольшая вариация, здесь мы присвоим наш массив еще одной переменной:

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

    Итерация объектов

    При итерации объектов имеет смысл рассмотреть два случая:

    Объект не Traversable (вернее, не определен внутренний обработчик get_iterator)

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

    • Для объявленных свойств PHP реоптимизирует хеш-таблицу свойств. Если вы все-таки итерируете объект, он должен реконструировать эту хеш-таблицу (что повышает использование памяти). Не то, чтобы вам следовало беспокоиться об этом, просто имейте в виду.
    • На каждой итерации хеш-таблица свойств будет получена заново, то есть PHP будет вызывать get_properties снова, и снова, и снова. Для «обычных» свойств это не так важно, но если свойства создаются динамически (это часто делают встроенные классы) — то таблица свойств будет пересчитываться каждый раз.
    Объект Traversable

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

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

    Другой необычный случай, который я не упомянул — PHP допускает возможность замены итерируемого объекта во время цикла. Вы можете начать с одним массивом и продолжить, заменив его на полдороге другим. Или начать с массивом, в затем заменить его объектом:

    Как видите, PHP просто начал обходить другую сущность, как только произошла замена.

    Изменение внутреннего указателя массива во время итерации

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

    Тут вы можете получить не то, что ожидали: если вызывать next или prev в теле цикла (в случае передачи по ссылке), вы увидите, что внутренний указатель переместился, но это никак не повлияло на поведение итератора. Причина в том, что foreach делает бекап текущей позиции и хеша текущего элемента в HashPointer после каждого прохода цикла. На следующей проходе foreach проверит, не менялась ли позиция внутреннего указателя и попытается восстановить ее, используя этот хеш.

    Давайте посмотрим что означает «попытается». Первый пример показывает, как изменение внутреннего указателя не меняет режим foreach:

    Теперь давайте попробуем сделать unset элементу, к которому обратится foreach при первом проходе (ключ 1):

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

    Имейте в виду, хеш — всего лишь хеш. Случаются коллизии. Попробуем теперь так:

    Работает так, как мы и ожидали. Мы удалили ключ EzFY (тот, где как раз был foreach), так что был сделан сброс. Также мы добавили дополнительный ключ, поэтому в конце мы видим 4.

    Читайте также:  Почему вентилятор не работает напрямую

    И вот тут приходит неведомое. Что произойдёт, если заменить ключ FYFY с FYFZ? Давайте попробуем:

    Сейчас цикл перешёл непосредственно к новому элементу, пропуская всё остальное. Это потому что ключ FYFY имеет коллизию с EzFY (вообще-то, все ключи из этого массива тоже). Более этого, элемент FYFY находится по тому же адресу в памяти, что и элемент EzFY который только что был удален. Так что для PHP это будет та же самая позиция с тем же хешом. Позиция «восстановлена» и происходит переход к концу массива.

    Источник

    НЕ работает foreach

    Не получается вывести с помощью конструкции forech двумерный массив.
    Получается только если использовать простой вывод.

    Как вывести с помощью этой конструкции?

    Добавлено через 21 минуту
    Прошу прощения,а что делать если двумерный ассоциативный массив такой,
    как этой конструкцией вывести весь двумерный массив, чтоб не писать постоянно, название каждого индекса,
    и каждый индекс по отдельности,в этом коде:

    Foreach работает с ошибкой
    reset($this->pass_error); foreach($this->pass_error as $temp)< $temp_s =.

    Не работает switch в конструкции foreach
    Помогите, пожалуйста, новичку с освоением самых-самых основ php. Почему switch в данном случае не.

    Цикл foreach в php, не работает
    У меня есть форма, в которой пользователь должен отправить электронное письмо. Он должен для этого.

    Подскажите пожалуйста, в чем может быть моя проблема, уже третий день мучаюсь, не могу сообразить:

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

    Почему во вложенных массивах он не может перебрать все элементы своего массива?

    Для демонстрации вот участок кода:

    Меню пользователя @ casual_visitor

    Ну, вообще говоря, у меня проблема конечно по-прежнему не решена, но посмотрев внимательнее мой код (весь), у меня сложилось впечатление что проблема в строках файла (мой файл robots), из которого делается массив $R. А именно все строки кроме последней имеют какие-то символы, типа: «\n» или «\t» — хотя удаление функцией rtrim (с указанием полного диапазона подобных символов) ничего не решило. это просто единственна причина которую я смог для себя выделить. Поэтому я подумал, что обсуждение в этой ветке далее вести бессмысленно (именно по foreach).

    а что ты имеешь ввиду: » работали с массивами без контроля выхода индекса за границы массива»? У меня вроде бы все в порядке, индексы никуда не выходят. или я чего-то не понял?

    Добавлено через 16 часов 19 минут
    Теперь проблема решилась точно, просто нужно было мне добавить условие прерывания цикла, тогда когда preg_match == true. Тогда код выдает правильный результат.
    А иначе он проходил по всем элементам, и каждый раз перезаписывал $cpt_all[$k][$url][‘robots’] — таким образом получалось, что в этом элементе содержался результат соответствующий последнему элементу массива $R.

    Как я так ступил. сам даже не пойму.

    Неправильно работает вывод массива через foreach
    Есть массив $childResult (print_r): Array ( => Array ( => 4 => Steam => 0 => => 1 ) =>.

    C AJAX не работает форма в которой есть цикл foreach as
    Друзья, помогите пожалуйста. Есть форма на сайте, она прекрасно пропускается через jquery валидацию.

    Foreach внутри foreach
    Посетители сайта видят список альбомов в мозаичном виде по 3 альбома в ряд, 12 альбомов на.

    Источник

    Оцените статью