- foreach
- Распаковка вложенных массивов с помощью list()
- Препарируем PHP. Как устроены while, foreach, array_walk и некоторые другие страшные слова
- Этап компиляции
- Парсер
- Перейдём к опкодам
- foreach
- Что тут происходит:
- for, while, .
- А можно ещё и так поитерировать
- array_walk с анонимной функцией
- Функция
- Основной код
- array_walk с использованием предопределённой функции
- Погружаемся чуть глубже
- Ну и немного замеров
- Подведём итоги
- PhpBuilder.ru
- ваш путеводитель по веб программированию
- Главное меню
- Уроки по PHP
- Урок 18. Способы перебора элементов массивов
foreach
(PHP 4, PHP 5, PHP 7)
Конструкция foreach предоставляет простой способ перебора массивов. Foreach работает только с массивами и объектами, и будет генерировать ошибку при попытке использования с переменными других типов или неинициализированными переменными. Существует два вида синтаксиса:
Первый цикл перебирает массив, задаваемый с помощью array_expression. На каждой итерации значение текущего элемента присваивается переменной $value и внутренний указатель массива увеличивается на единицу (таким образом, на следующей итерации цикла работа будет происходить со следующим элементом).
Второй цикл будет дополнительно соотносить ключ текущего элемента с переменной $key на каждой итерации.
Когда оператор foreach начинает исполнение, внутренний указатель массива автоматически устанавливается на первый его элемент Это означает, что нет необходимости вызывать функцию reset() перед использованием цикла foreach.
Так как оператор foreach опирается на внутренний указатель массива, его изменение внутри цикла может привести к непредсказуемому поведению.
Для того, чтобы напрямую изменять элементы массива внутри цикла, переменной $value должен предшествовать знак &. В этом случае значение будет присвоено по ссылке.
Указатель на $value возможен, только если на перебираемый массив можно ссылаться (т.е. если он является переменной). Следующий код не будет работать:
Ссылка $value на последний элемент массива остается даже после того, как оператор foreach завершил работу. Рекомендуется уничтожить ее с помощью функции unset() .
Оператор foreach не поддерживает возможность подавления сообщений об ошибках с помощью префикса ‘@’.
Вы могли заметить, что следующие конструкции функционально идентичны:
= array( «one» , «two» , «three» );
reset ( $arr );
while (list(, $value ) = each ( $arr )) <
echo «Значение: $value
\n» ;
>
foreach ( $arr as $value ) <
echo «Значение: $value
\n» ;
>
?>
Следующие конструкции также функционально идентичны:
= array( «one» , «two» , «three» );
reset ( $arr );
while (list( $key , $value ) = each ( $arr )) <
echo «Ключ: $key ; Значение: $value
\n» ;
>
foreach ( $arr as $key => $value ) <
echo «Ключ: $key ; Значение: $value
\n» ;
>
?>
Вот еще несколько примеров, демонстрирующие использование оператора:
/* Пример 1: только значение */
$a = array( 1 , 2 , 3 , 17 );
foreach ( $a as $v ) <
echo «Текущее значение переменной \$a: $v .\n» ;
>
/* Пример 2: значение (для иллюстрации массив выводится в виде значения с ключом) */
$a = array( 1 , 2 , 3 , 17 );
$i = 0 ; /* только для пояснения */
foreach ( $a as $v ) <
echo «\$a[ $i ] => $v .\n» ;
$i ++;
>
/* Пример 3: ключ и значение */
$a = array(
«one» => 1 ,
«two» => 2 ,
«three» => 3 ,
«seventeen» => 17
);
foreach ( $a as $k => $v ) <
echo «\$a[ $k ] => $v .\n» ;
>
/* Пример 4: многомерные массивы */
$a = array();
$a [ 0 ][ 0 ] = «a» ;
$a [ 0 ][ 1 ] = «b» ;
$a [ 1 ][ 0 ] = «y» ;
$a [ 1 ][ 1 ] = «z» ;
foreach ( $a as $v1 ) <
foreach ( $v1 as $v2 ) <
echo » $v2 \n» ;
>
>
/* Пример 5: динамические массивы */
foreach (array( 1 , 2 , 3 , 4 , 5 ) as $v ) <
echo » $v \n» ;
>
?>
Распаковка вложенных массивов с помощью list()
(PHP 5 >= 5.5.0, PHP 7)
В PHP 5.5 была добавлена возможность обхода массива массивов с распаковкой вложенного массива в переменные цикла, передав list() в качестве значения.
foreach ( $array as list( $a , $b )) <
// $a содержит первый элемент вложенного массива,
// а $b содержит второй элемент.
echo «A: $a ; B: $b \n» ;
>
?>
Результат выполнения данного примера:
Можно передавать меньшее количество элементов в list() , чем находится во вложенном массиве, в этом случае оставшиеся значения массива будут проигнорированы:
foreach ( $array as list( $a )) <
// Обратите внимание на отсутствие $b.
echo » $a \n» ;
>
?>
Результат выполнения данного примера:
Если массив содержит недостаточно элементов для заполнения всех переменных из list() , то будет сгенерировано замечание об ошибке:
foreach ( $array as list( $a , $b , $c )) <
echo «A: $a ; B: $b ; C: $c \n» ;
>
?>
Источник
Препарируем PHP. Как устроены while, foreach, array_walk и некоторые другие страшные слова
Дело было вечером, делать было нечего. Самое время устроить небольшой разбор того, чем изнутри отличаются некоторые способы перебора массивов в PHP.
Исходники от master ветки (это сейчас 7.4 с вкраплениями 8)
Генератор опкодов от php 7.3.0.
Замеры производились на 7.3.6.
Дисклеймер для зануд: упоминание пары наносекунд и тактов процессора – это такой полемический приём под названием «гипербола».
Может быть, на самом деле, там десятки или сотни наносекунд и тысячи тактов, но это всё равно настолько малые величины, что необходимость экономить на них говорит о том, что что-то в вашем коде не так.
Этап компиляции
for, foreach, do и while являются ключевыми словами языка, тогда как функции array_* – это функции стандартной библиотеки. Следовательно, при прочих равных, по первым парсер отработает на пару наносекунд быстрее.
Парсер
До токена statement путь будет одинаков для всех
Циклы определены на уровне statement:
Отличие for_exprs от просто expr только в том, что для первого допустима запись нескольких expr через запятую.
foreach_variable – это конструкция, которая помимо просто variable, также отслеживает распаковку с помощью list или [].
for_statement, foreach_statement, while_statement отличаются от стандартного statement тем, что в них добавлена возможность разбора альтернативного синтаксиса:
Вызов функций закопан гораздо глубже:
callable_variable, хм… Забавно, не правда ли? 🙂
Перейдём к опкодам
Для примера давайте возьмём простой перебор индексированного массива с печатью каждого ключа и значения. Понятно, что использование for, while и do для такой задачи не оправдано, но у нас цель просто показать внутреннее устройство.
foreach
Что тут происходит:
5, а значение в !1. Либо, если достигнут конец массива, переходит к инструкции 7.
Инструкции 3-6 не особо интересны. Тут происходит вывод и возврат к FE_FETCH_R.
FE_FREE: уничтожает итератор.
for, while, .
На самом деле это частный случай while
На самом деле это частный случай if+goto
Опкоды для всех трёх случаев будут практически идентичны. Разве что в случае с if, JMPNZ поменяется на пару JMPZ+JMP из-за входа в тело if‘а.
Для цикла do опкоды будут незначительно отличаться из-за его постпроверочной природы.
Опкодов, ожидаемо, стало больше.
0-2: вычисление $length.
3: $i = 0
4, 10, 11: первичная проверка условия выхода и, собственно, либо выход, либо переход к телу цикла.
5(FETCH_DIM_R): $arr[$i]
6-7: вывод
8-9: $i++ (обратите внимание, что опкод инкремента в любом случае возвращает значение, даже если оно не нужно. Поэтому требуется дополнительная инструкция FREE, для его очистки).
10-11: проверка условия выхода после инкремента.
А можно ещё и так поитерировать
Этот вариант хорош тем, что подходит для итерации по массиву с любыми ключами, а не только с монотонно возрастающими целыми числами.
Функции reset, next и key довольно легковесные, но накладные расходы на их вызов всё же есть. И, как мы увидим дальше, расходы эти велики.
Хотя такой подход очень сильно напоминает принцип работы foreach, между ними есть два принципиальных отличия.
1) Тогда как reset, next и key (и current тоже) работают напрямую с внутренним указателем массива, foreach использует собственный итератор и не меняет состояние внутреннего указателя.
2) При использовании foreach для итерации по значению, что бы вы не делали с массивом внутри цикла, проитерирован будет именно первоначальный набор данных
Что будет при итерации по ссылке, можно почитать в этом RFC. Там всё не очень просто.
array_walk с анонимной функцией
Так как используется пользовательская функция, то будет дополнительный набор опкодов.
Функция
Основной код
Поскольку array_walk, как и остальные функции стандартной библиотеки, является интринсиком, то в скомпилированных опкодах механизм итерации отсутствует.
INIT_FCALL: инициализируем вызов array_walk
SEND_REF: кладём ссылку на массив на стек вызова
DECLARE_LAMBDA_FUNCTION: объявляем анонимную функцию
SEND_VAL: кладём анонимную функцию на стек вызова
DO_ICALL: запускаем array_walk на выполнение
Далее там происходит магия с вызовом нашей лямбды для каждого элемента массива.
array_walk с использованием предопределённой функции
Не сильно отличается от вызова с анонимной, разве только чуть меньше накладных расходов на создание лямбды во время исполнения.
Выводы банальны. foreach заточен под итерирование массивов, тогда как остальные циклы – просто обёртка над if+goto.
Функции же стандартной библиотеки работают по принципу чёрного ящика.
Погружаемся чуть глубже
Для начала рассмотрим случай с for и его опкодом FETCH_DIM_R, использующимся для извлечения значения по ключу. Извлечение элемента идёт через поиск в хеш-таблице (ZEND_HASH_INDEX_FIND). В нашем случае извлечение идёт из упакованного массива (ключи – непрерывная числовая последовательность) – это довольно лёгкая и быстрая операция. Для неупакованных массивов она будет чуть подороже.
Теперь foreach (FE_FETCH_R). Тут все банально:
- Проверяем, не вышел ли указатель итератора за пределы массива
- Извлекаем текущие ключ и значение
- Инкрементируем указатель
Ну а что происходит с функциями типа array_walk? Хоть все они делают разные вещи, но в основе у них у лежит один и тот же принцип.
Если совсем упрощённо, то (псевдокод):
На самом деле внутри всё сложнее, но суть одна – идёт довольно быстрый перебор хеш-таблицы без участия виртуальной машины PHP (не учитывая вызова пользовательской функции).
Ну и немного замеров
А то ведь какая же статья без замеров (по памяти получилось настолько одинаково, что убрал её измерение).
В качестве массива, по традиции, возьмём zend_vm_execute.h на 70.108 строк.
Каждое измерение запускал раз по 10, выбирая наиболее часто встречающееся по первым 4-м цифрам.
Подведём итоги
Анализируя результаты, не забываем учитывать, что они получены на 10 проходах по массиву из 70 тысяч элементов.
Абсолютным антигероем оказалась «эмуляция» foreach с помощью next/key. Не делайте так без крайней на то необходимости.
array_walk с лямбдой дышит ему в спину, но тут есть нюанс. Грядущий JIT может кардинально изменить ситуацию. А может и не изменить. Интересно будет посмотреть.
array_walk с использованием готовой функции – крепкий середнячок.
Так как при итерации по ссылке foreach работает несколько иначе (использует опкод FE_FETCH_RW вместо FE_FETCH_R), то сделал для него отдельный замер. Он действительно чуть-чуть быстрее получился.
Как оказалось, создание лямбды на лету – не самая дешёвая операция. Казалось бы, создаётся она всего 10 раз. Надо будет поизучать.
Все остальные способы показали примерно одинаковые результаты, с очень незначительным разрывом.
Спасибо за внимание!
Если есть предложения, что ещё можно «поковырять» – пишите в комментариях. Я пока подумываю о лямбдах – уж очень странна такая просадка производительности.
UPD
Добавил замер для array_walk со статической лямбдой. Разницы не видно.
Источник
PhpBuilder.ru
ваш путеводитель по веб программированию
Главное меню
Уроки по PHP
Урок 18. Способы перебора элементов массивов
При работе с массивами в языке программирования php множество задач решается с помощью перебора их элементов. Для этого используют рассмотренные в прошлых уроках циклические конструкции (for, whilе…) или конструкцию foreach, которая была специально создана для работы с массивами.
Давайте для начала рассмотрим способ перебора массива с помощью цикла whilе:
В результате работы этого скрипта мы увидим на экране следующую информацию:
Скорее всего в данном примере Вы столкнулись с несколькими незнакомыми или непонятными моментами. Поэтому давайте рассмотрим этот скрипт более детально.
Вначале создается массив $goroda с 3 элементами:
$goroda [0] = “Токио”,
$goroda [1] = “Пекин”,
$goroda [2] = “Москва”
Потом инициализируются 2 переменные: $index и $elements. Первая выступает ключом (индексом) для последующего обращения внутри цикла к элементам массива. Эта переменная получила значение 0, так как индексация массивов в php начинается с нуля.
Переменная $elements получила значение, которое вернула встроенная функция count(). Встроенная функция count() возвращает количество элементов массива, имя которого задается в аргументе. В примере 3 элемента, поэтому значение $elements будет равняться трем.
Итерация в цикле согласно условию выполняется до тех пор, пока индекс элементов не будет равнятся максимальному количеству элементов в перебираемом масссиве (не забываем, что переменная $index равна нулю, а не единице). На экран при помощи команды echo выводится результат операции конкатенации. В конце выполняется инкремент (увеличение переменной $index на единицу). Не забывайте об этом нюансе, так как он позволяет работать с последующими элементами массива и обеспечивает прекращение работы цикла, когда заданное условие выполнено.
Пример выполнения этой ж задачи с помощью цикла for:
Циклы удобно использовать для перебора индексных массивов, так как выполняются операции с целочисленными индексами. Эту задачу также можно решить и с помощью конструкции foreach. Всего есть 2 способа записи данной конструкции:
Ключ элемента передается в переменную $key, значение в свою очередь присваивается переменной $value. В то время как в первом варианте записи доступно только значение элемента, во втором доступным является еще и его ключ. Пример:
Чаще всего конструкцию foreach в php используют для перебора ассоциативных массивов. Рассмотрим следующий пример:
В некоторых случаях способы перебора элементов массива комбинируют. Давайте организуем перебор элементов многомерного массива:
Данный скрипт выведет на экран в виде удобной таблицы массив $polzovateli. Если Вам необходимо просмотреть структуру и значения элементов массива без красивого вывода, то используйте встроенную функцию print_r(). У нее следующий синтаксис:
bool print_r (mixed expression[, bool return]);
Пример использования на практике:
Рекомендуем самостоятельно поэксперементировать с данной php функцией.
Источник