php array walk recursive
Препарируем 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: уничтожает итератор.
На самом деле это частный случай while
На самом деле это частный случай if+goto
Опкоды для всех трёх случаев будут практически идентичны. Разве что в случае с if, JMPNZ поменяется на пару JMPZ+JMP из-за входа в тело if‘а.
Для цикла do опкоды будут незначительно отличаться из-за его постпроверочной природы.
А можно ещё и так поитерировать
Этот вариант хорош тем, что подходит для итерации по массиву с любыми ключами, а не только с монотонно возрастающими целыми числами.
Функции 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). Тут все банально:
Если совсем упрощённо, то (псевдокод):
На самом деле внутри всё сложнее, но суть одна – идёт довольно быстрый перебор хеш-таблицы без участия виртуальной машины 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 со статической лямбдой. Разницы не видно.
array_walk_recursive
array_walk_recursive — Рекурсивно применяет пользовательскую функцию к каждому элементу массива
Описание
Список параметров
Если требуется, чтобы функция callback изменила значения в массиве, определите первый параметр callback как ссылку. Тогда все изменения будут применены к элементам массива.
Возвращаемые значения
Возвращает true в случае успешного выполнения или false в случае возникновения ошибки.
Примеры
Пример #1 Пример использования array_walk_recursive()
Результат выполнения данного примера:
Смотрите также
User Contributed Notes 27 notes
Since this is only mentioned in the footnote of the output of one of the examples, I feel it should be spelled out:
* THIS FUNCTION ONLY VISITS LEAF NODES *
That is to say that if you have a tree of arrays with subarrays of subarrays, only the plain values at the leaves of the tree will be visited by the callback function. The callback function isn’t ever called for a nodes in the tree that subnodes (i.e., a subarray). This has the effect as to make this function unusable for most practical situations.
How to modify external variable from inside recursive function using userdata argument.
// result
// 11 : 1
// 12 : 1
// 2 : 1
// counter: 0
// result
// 11 : 1
// 12 : 2
// 2 : 1
// counter : 0
// result
// 11 : 1
// 12 : 2
// 2 : 3
// counter : 3
Unfortunately the PHP example given doesn’t do this. It actually took me a while to figure out why my function wasn’t changing the original array, even though I was passing by reference.
One other silly thing you might try first is something like this:
I use RecursiveIteratorIterator with parameter CATCH_GET_CHILD to iterate on leafs AND nodes instead of array_walk_recursive function :
The description says «If funcname needs to be working with the actual values of the array, specify the first parameter of funcname as a reference.» This isn’t necessarily helpful as the function you’re calling might be built in (e.g. trim or strip_tags). One option would be to create a version of these like so.
multidimensional array to single array
Array ( [0] => 2 [1] => 4 [2] => 2 [3] => 7 [4] => 3 [5] => 6 [6] => 5 [7] => 4 )
array_walk_recursive itself cannot unset values. Even though you can pass array by reference, unsetting the value in the callback will only unset the variable in that scope.
A simple solution for walking a nested array to obtain the last set value of a specified key:
I needed to add or modify values in an array with unknown structure. I was hoping to use array_walk_recursive for the task, but because I was also adding new nodes I came up with an alternate solution.
I decided to add to the previous PHP 4 compatible version of array_walk_recursive() so that it would work within a class and as a standalone function. Both instances are handled by the following function which I modified from omega13a at sbcglobal dot net.
The following example is for usage within a class. To use as a standalone function take it out of the class and rename it. (Example: array_walk_recursive_2)
array_walk_recursive
array_walk_recursive — Рекурсивно применяет пользовательскую функцию к каждому элементу массива
Описание
Список параметров
Если требуется, чтобы функция funcname изменила значения в массиве, определите первый параметр funcname как ссылку. Тогда все изменения будут применены к элементам массива.
Возвращаемые значения
Возвращает TRUE в случае успешного завершения или FALSE в случае возникновения ошибки.
Примеры
Пример #1 Пример использования array_walk_recursive()
Результат выполнения данного примера:
Смотрите также
Коментарии
This is a peice of code I wrote that appears to create this function for PHP 4.
If there is anything wrong with it, please email me.
I decided to add to the previous PHP 4 compatible version of array_walk_recursive() so that it would work within a class and as a standalone function. Both instances are handled by the following function which I modified from omega13a at sbcglobal dot net.
The following example is for usage within a class. To use as a standalone function take it out of the class and rename it. (Example: array_walk_recursive_2)
To egingell at sisna dot com:
It’s a nice function, because I was searching for something to change the keys of a multiple dimension array.
This function has a serious bug, which is still not fixed as of the PHP 5.2.5 release. After you call it, it can accidentally modify your original array. Save yourself hours of frustration by reading on.
The bug is here: http://bugs.php.net/bug.php?id=42850, and it looks like it will be fixed for 5.3.
If the array that you walk contains other array elements, they will be turned into references. This will happen even if the callback function doesn’t take its first argument by reference, and doesn’t do anything to the values.
The description says «If funcname needs to be working with the actual values of the array, specify the first parameter of funcname as a reference.» This isn’t necessarily helpful as the function you’re calling might be built in (e.g. trim or strip_tags). One option would be to create a version of these like so.
If you don’t really particularly care about the keys of an array, you can capture all values quite simply:
/*
* Array
* (
* [0] => woof
* [1] => hiss
* [2] => purr
* [3] => kssksskss
* )
*/
?>
[EDIT BY danbrown AT php DOT net: In a note added by ‘FaustoFilho’ on 17-MAY-2011, the following information was appended to this note.
[If you intend to use this] «function to extract the last key value of an array, don’t forget to insert a currency sign ($) signal before ‘$output’ at 7th line.
This value must be a variable, and if you forgot to assign this signal, your code won’t work, displaying an error like this:
Parse error: syntax error, unexpected ‘)’, expecting T_PAAMAYIM_NEKUDOTAYIM in /path/to/script.php on line 7.»]
Unfortunately the PHP example given doesn’t do this. It actually took me a while to figure out why my function wasn’t changing the original array, even though I was passing by reference.
One other silly thing you might try first is something like this:
I needed to add or modify values in an array with unknown structure. I was hoping to use array_walk_recursive for the task, but because I was also adding new nodes I came up with an alternate solution.
array_walk
(PHP 4, PHP 5, PHP 7, PHP 8)
array_walk — Применяет заданную пользователем функцию к каждому элементу массива
Описание
Список параметров
Если требуется, чтобы функция callback изменила значения в массиве, определите первый параметр callback как ссылку. Тогда все изменения будут применены к элементам оригинального массива.
Потенциально изменены могут быть только значения массива array ; структура самого массива не может быть изменена, то есть нельзя добавить, удалить или поменять порядок элементов. Если callback-функция не соответствует этому требованию, поведение данной функции станет неопределённым и непредсказуемым.
Возвращаемые значения
Возвращает true
Ошибки
Примеры
Пример #1 Пример использования array_walk()
Результат выполнения данного примера:
Смотрите также
User Contributed Notes 34 notes
PHP ignored arguments type when using array_walk() even if there was
declare( strict_types = 1 );
butter: 5
meat: 7
banana: 3
whilst the expecting output is :
Fatal error: Uncaught TypeError: Argument 1 passed to test_print() must be of the type integer
because «butter» => 5.3 is float
I asked someone about it and they said «this was caused by the fact that callbacks called from internal code will always use weak type». But I tried to do some tests and this behavior is not an issue when using call_user_func().
Calling an array Walk inside a class
If the class is static:
array_walk($array, array(‘self’, ‘walkFunction’));
or
array_walk($array, array(‘className’, ‘walkFunction’));
Otherwise:
array_walk($array, array($this, ‘walkFunction’));
There is a note about 3 years ago regarding using this for trimming. array_map() may be cleaner for this. I haven’t checked the time/resource impact:
Correction for the speed test from zlobnygrif.
// Test results
$array1 = test ( ‘array_walk’ );
$array2 = test ( ‘array_walk_list_each’ );
$array3 = test ( ‘array_walk_foreach1’ );
$array4 = test ( ‘array_walk_foreach2’ );
In response to ‘ibolmo’, this is an extended version of string_walk, allowing to pass userdata (like array_walk) and to have the function edit the string in the same manner as array_walk allows, note now though that you have to pass a variable, since PHP cannot pass string literals by reference (logically).
// We can make that with this simple FOREACH loop :
$fruits = array(«d» => «lemon», «a» => «orange», «b» => «banana», «c» => «apple»);
Array
(
[d] => fruit: lemon
[a] => fruit: orange
[b] => fruit: banana
[c] => fruit: apple
)
For those that think they can’t use array_walk to change / replace a key name, here you go:
I wanted to walk an array and reverse map it into a second array. I decided to use array_walk because it should be faster than a reset,next loop or foreach(x as &$y) loop.
Don’t forget about the array_map() function, it may be easier to use!
Here’s how to lower-case all elements in an array:
It can be very useful to pass the third (optional) parameter by reference while modifying it permanently in callback function. This will cause passing modified parameter to next iteration of array_walk(). The exaple below enumerates items in the array:
Array
(
[0] => 1 lemon
[1] => 2 orange
[2] => 3 banana
[3] => 4 apple
)
$num is: 1
As a conclusion, using references with array_walk() can be powerful toy but this should be done carefully since modifying third parameter outside the array_walk() is not always what we want.
array_walk does not work on SplFixedArray objects:
= new SplFixedArray ( 2 );
$array [ 0 ] = ‘test_1’ ;
$array [ 1 ] = ‘test_2’ ;
Unfortunately I spent a lot of time trying to permanently apply the effects of a function to an array using the array_walk function when instead array_map was what I wanted. Here is a very simple though effective example for those who may be getting overly frustrated with this function.
Prefix array values with keys and retrieve as a glued string, the original array remains unchanged. I used this to create some SQL queries from arrays.
Using lambdas you can create a handy zip function to zip together the keys and values of an array. I extended it to allow you to pass in the «glue» string as the optional userdata parameter. The following example is used to zip an array of email headers:
/*
From: Matthew Purdon
Reply-To: Matthew Purdon
Return-path:
X-Mailer: PHP5.3.2
Content-Type: text/plain; charset=»UTF-8″
*/
?>
// Test results
$array1 = test ( ‘array_walk’ );
$array2 = test ( ‘array_walk_list_each’ );
$array3 = test ( ‘array_walk_foreach1’ );
$array4 = test ( ‘array_walk_foreach2’ );
PHP 5.5 array_walk looks pretty good but list each is more and more quickly.
For completeness one has to mention the possibility of using this function with PHP 5.3 closures:
You can use lambda function as a second parameter:
I was looking for trimming all the elements in an array, I found this as the simplest solution:
And to set allow_call_time_pass_reference to true in php.ini won’t work, according to http://bugs.php.net/bug.php?id=19699 thus to work around:
example with closures, checking and deleting value in array:
You can change the key or value with array_walk if you use the temporal returned array in global inside the function. For example:
$array = [‘a’=>10, ‘b’=>20];
$sequence = array ();
$newArray = array_values(array_walk($array, ‘fn’));
No need to concern about the place of the internal pointer for the baby array. You have now rewinded, 0 based new array, string key one instead.
If you want to add values with same key from two arrays :
echo «» ;
?>
This will output:
«orange» => 3,
«banana» => 3,
«apple» => 5
here is a simple and yet easy to use implementation of this function.
the ‘original’ function has the problem that you can’t unset a value.
with my function, YOU CAN!
limitations: it only can run user defined functions.
i hope you like it!
You want to get rid of the whitespaces users add in your form fields.
Simply use.
so.
$obj = new SomeVeryImportantClass;
$obj->mungeFormData($_POST);
___
eNc
For all those people trying to shoe-horn trim() into array_walk() and have found all these tricks to work around the issue with array_walk() passing 2 parameters to the callback.
Check out array_map().
It’s all sorts of win.
For the record. I’m one of these people and after 15 years of php development I’m pleased to say that there’s still things I’m learning. 🙂 I just found out about array_map() myself.
return true ; // success!
> // arrayWalk()
So, still some work left.
Beware that «array ($this, method)» construct. If you’re wanting to alter members of the «$this» object inside «method» you should construct the callback like this:
if you want to modify every value of an multidimensional array use this function used here:
Array ( [ 1 ] => 1test [ 2 ] => 2test [ 3 ] => Array ( [ 1 ] => 11test [ 2 ] => 12test [ 3 ] => 13test ) )
?>
PHP | Функция array_walk_recursive ()
Функция array_walk_recursive () является встроенной функцией в PHP. Функция array_walk_recursive () проходит по всему массиву независимо от позиции указателя и рекурсивно применяет функцию обратного вызова или пользовательскую функцию к каждому элементу массива. Ключи и значения элемента массива являются параметрами в функции обратного вызова. Разница между этой функцией и функцией array_walk () заключается в том, что она будет преобразовываться в более глубокие массивы (массив внутри массива).
Синтаксис:
Параметры: Эта функция принимает три параметра, как описано ниже:
Возвращаемое значение: эта функция возвращает логическое значение. Возвращает TRUE в случае успеха или FALSE в случае неудачи.
Ниже программы иллюстрируют функцию array_walk_recursive ():
Программа 1 :
// PHP программа для иллюстрации
// функция array_walk_recursive ()
// определяемая пользователем функция обратного вызова
// вызов функции array_walk_recursive () без
// дополнительный параметр
Выход:
Программа 2 :
// PHP программа для иллюстрации
// функция array_walk_recursive ()
// определяемая пользователем функция обратного вызова
// с дополнительным параметром
// вызов функции array_walk_recursive () с
// дополнительный параметр