php unset in foreach
Подводный камень в foreach($items as &$item)
Многие любят писать такие конструкции в том или ином виде, каждый сталкивался:
Но не многие подозревают о том, какая опасность тут скрывается.
Рассмотрим пример.
Вася Пупкин взял массив, прошелся по нему, увеличив на два все элементы:
Посмотрел дамп, увидел что задача решена, и ушел довольный:
Спустя некоторое время, Петрович решил дополнить этот участок кода другим перебором, дописав ниже:
Посмотрел, что его задача тоже решена, и с чувством выполненного долга закрыл файл:
Спустя какое-то время, стали вылезать необъяснимые баги. Почему?
Сделаем в конце кода var_dump($items):
30! Вася Пупкин клянётся, что проверял. Почему было 32, а после кода Петровича 30?
Причина кроется в амперсанде. Он сообщает, что на отмеченные данные ссылается кто-то ещё. Уходя, Вася не подтёр за собой временную переменную, которую использовал для перебора ($item). Переменная использовалась с разрешением на изменение источника («&»), которое также называют «присваиванием по ссылке». Он был уверен, что переменная будет использоваться только внутри цикла. Петрович, используя переменную с таким же именем, в ходе своего перебора, менял её значение, и каждый раз менялось то место, где эта переменная хранилась. А хранилась она там же, где последний элемент массива Пупкина.
Конечно, в случай в статье утрирован. На практике такие связи могут быть очень сложными, особенно если проект недорогой, и в нём участвуют недостаточно опытные и разрозненные веб-разработчики.
О тонкостях работы foreach в PHP
В недавнем дайджесте интересных ссылок о PHP я обнаружил ссылку на комментарий Никиты Попова на StackOverflow, где он подробно рассказывает о механизме «под капотом» управляющей конструкции foreach.
Поскольку foreach действительно иногда работает более, чем странным образом, я счел полезным сделать перевод этого ответа.
Внимание: этот текст подразумевает наличие базовых знаний о функциональности zval’ов в PHP, в частности вы должны знать что такое refcount и is_ref.
foreach работает с сущностями разных типов: с массивами, с простыми объектами (где перечисляются доступные свойства) и с Traversable-объектами (вернее, объектами, у которых определен внутренний обработчик get_iterator). Здесь мы, в основном, говорим о массивах, но я скажу и об остальных в самом конце.
Прежде чем приступить, пара слов о массивах и их обходе, важная для понимания контекста.
Как работает обход массивов
Массивы в PHP являются упорядоченными хеш-таблицами (элементы хеша объединены в двусвязный список) и foreach обходит массив, следуя указанному порядку.
Таким образом, внешние указатели массива могут быть использованы только когда вы полностью уверены, что при обходе никакого пользовательского кода выполняться не будет. А такой код может оказаться в самом неожиданном месте, типа обработчика ошибок или деструктора. Вот почему в большинстве случаев PHP приходится использовать внутренний указатель вместо внешнего. Если бы это было иначе, PHP мог бы упасть из-за segmentation fault, как только пользователь начнет делать что-нибудь необычное.
Проблема внутреннего указателя в том, что он является частью HashTable. Так что, когда вы изменяете его, HashTable меняется вместе с ним. И коль скоро обращение к массивам в PHP делается по значению (а не по ссылке), вы вынуждены копировать массив, чтобы в цикле обходить его элементы.
Простой пример, показывающий важность копирования (кстати, не такая большая редкость), это вложенная итерация:
Здесь вы хотите чтобы оба цикла были независимым, а не хитро перебрасывались одним указателем.
Итак, мы дошли до foreach.
Обход массива в foreach
Теперь вы знаете, для чего foreach приходится создавать копию массива, прежде чем обойти его. Но это явно не вся история. Сделает PHP копию или нет, зависит от нескольких факторов:
Итак, это первая часть тайны: функция копирования. Вторая часть это то, как текущая итерация выполняется, и она тоже довольно странная. «Обычный» образец итерации, который вы уже знаете (и который часто используется в PHP — отдельно от foreach) выглядит примерно так (псевдокод):
итерация foreach выглядит немного иначе:
Такой режим работы foreach также является причиной, по которой внутренний указатель массива переходит к следующему элементу, если текущий удалён, а не к предыдущему (как вы могли бы ожидать). Всё сделано так, чтобы отлично работать с foreach (но, очевидно, со всем остальным будет работать не так хорошо, пропуская элементы).
Последствия для кода
Первое следствие вышеописанного поведения в том, что foreach копирует итерируемый массив в многих случаях (медленно). Но отриньте страх: я пробовал удалить требование копирования и не смог увидеть ускорения работы нигде, кроме искусственных бенчмарков (в которых итерация происходила в два раза быстрее). Похоже, люди просто не итерируют достаточно много.
Второе следствие в том, что обычно не должно быть других следствий. Поведение foreach, в основном, вполне понятно пользователю и просто работает как следует. Вас не должно волновать, как происходит копирование (и происходит ли оно вообще), и в какой конкретно момент времени перемещается указатель.
И третье следствие — и тут мы как раз подходим к вашим проблемам — в том, что иногда мы видим очень странное поведение, которое трудно понять. Это происходит конкретно тогда, когда вы пытаетесь модифицировать сам массив, который вы обходите в цикле.
Большую коллекцию поведения в пограничных случаях, которые появляются, когда вы модифицируете массив в ходе итерации, можно найти в тестах PHP. Вы можете начать с этого теста, после чего изменять 012 на 013 в адресе, и так далее. Вы увидите, как поведение foreach будет проявляться в разных ситуациях (всякие комбинации ссылок и.т.д.).
А сейчас вернёмся к вашим примерам:
Та же ситуация, что и в первом тесте.
Но эти примеры недостаточно убедительны. Поведение начинает быть по настоящему непредсказуемым, когда вы используете current в цикле:
Теперь попробуем сделать небольшое изменение:
Здесь у нас is_ref=1, так что массив не копирован (так как и выше). Но сейчас когда есть is_ref, массив больше не нужно разделять, передавая по ссылке к current. Теперь current и foreach работают с одним массивом. Вы видите массив сдвинутым на единицу как раз из-за того, как foreach обращается с указателем.
То же самое вы увидите, когда будете делать обход массива по ссылкам:
Еще одна небольшая вариация, здесь мы присвоим наш массив еще одной переменной:
Итерация объектов
При итерации объектов имеет смысл рассмотреть два случая:
Объект не Traversable (вернее, не определен внутренний обработчик get_iterator)
В этом случае итерация происходит почти так же, как у массивов. Та же семантика копирования. Единственное отличие: foreach запустит некий дополнительный код, чтобы пропустить свойства, недоступные в текущей области видимости. Еще пара интересных фактов:
Объект 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 это будет та же самая позиция с тем же хешом. Позиция «восстановлена» и происходит переход к концу массива.
Php unset in foreach
You can also use the alternative syntax for the foreach cycle:
I cannot stress this point of the documentation enough! Here is a simple example of exactly why this must be done:
Even though it is not mentioned in this article, you can use «break» control structure to exit from the «foreach» loop.
WARNING: Looping through «values by reference» for «extra performance» is an old myth. It’s actually WORSE!
?>
Which do you think is faster?
Lots of people think the answer is two() because it uses «reference to value, which it doesn’t have to copy each value when it loops».
Well, that’s totally wrong!
Here’s what actually happens:
Alright, so what’s the second version doing? The beloved «iterate values by reference»?
— This function takes an array as argument ($arr).
— The array function argument itself isn’t passed by reference, so the function knows it isn’t allowed to modify the original at all.
— Then the foreach loop happens. The array itself wasn’t passed by reference to the function, so PHP knows that it isn’t allowed to modify the outside array.
— But it also sees that you want to look at all VALUES by reference (&$val), so PHP says «Uh oh, this is dangerous. If we just give them references to the original array’s values, and they assign some new value to their reference, they would destroy the original array which they aren’t allowed to touch!».
— So PHP makes a FULL COPY of the ENTIRE array and ALL VALUES before it starts iterating. YIKES!
Therefore: STOP using the old, mythological «&$val» iteration method! It’s almost always BAD! With worse performance, and risks of bugs and quirks as is demonstrated in the manual.
You can always manually write array assignments explicitly, without references, like this:
unset
(PHP 4, PHP 5, PHP 7, PHP 8)
unset — Unset a given variable
Description
unset() destroys the specified variables.
The behavior of unset() inside of a function can vary depending on what type of variable you are attempting to destroy.
If a globalized variable is unset() inside of a function, only the local variable is destroyed. The variable in the calling environment will retain the same value as before unset() was called.
The above example will output:
If a variable that is PASSED BY REFERENCE is unset() inside of a function, only the local variable is destroyed. The variable in the calling environment will retain the same value as before unset() was called.
The above example will output:
If a static variable is unset() inside of a function, unset() destroys the variable only in the context of the rest of a function. Following calls will restore the previous value of a variable.
The above example will output:
Parameters
The variable to be unset.
Return Values
No value is returned.
Examples
Example #1 unset() example
Example #2 Using (unset) casting
The above example will output:
Notes
Note: Because this is a language construct and not a function, it cannot be called using variable functions.
It is possible to unset even object properties visible in current context.
When using unset() on inaccessible object properties, the __unset() overloading method will be called, if declared.
See Also
User Contributed Notes 31 notes
This doesn’t apply to properties of objects that have __isset() methods that visibly change object state or __unset() methods that don’t properly check their arguments or have extra side effects.
The latter case means that __unset shouldn’t do more than what it says on the tin, and also has the responsibility for checking (possibly using __isset()) that what it’s being asked to do makes sense.
The former case is just plain bad design.
if you try to unset an object, please be careful about references.
Objects will only free their resources and trigger their __destruct method when *all* references are unsetted.
Even when they are *in* the object. sigh!
class A <
function __destruct () <
echo «cYa later!!\n» ;
>
>
echo «Finally that thing is gone\n» ;
?>
Of course the object completely dies at the end of the script.
Since unset() is a language construct, it cannot be passed anything other than a variable. It’s sole purpose is to «unset» this variable, ie. to remove it from the current scope and destroy it’s associated data. This is true especially for reference variables, where not the actual value is destroyed but the reference to that value. This is why you can’t wrap ‘unset()’ in a user defined function: You would either unset a copy of the data if the parameter is passed by value, or you would just unset the reference variable within the functions scope if the parameter is passed by reference. There is no workaround for that, as you cannot pass ‘scope’ to a function in PHP. Such a function can only work for variables that exist in a common or global scope (compare ‘unset($_GLOBALS[variable])’).
I don’t know how PHP handles garbage collection internally, but I guess this behavior can result in a huge memory leak: if a value variable goes out of scope with a second variable still holding a reference to the in-memory value, then unsetting that reference would still hold the value in memory but potentially unset the last reference to that in-memory data, hence: occupied memory that is rendered useless as you cannot reference it anymore.
A sample how to unset array elements from an array result coming from a mysql request. In this sample it is checking if a file exists and removes the row from the array if it not exists.
1.
it gets the array from the table (mysql)
3.
unset if record does not exist
Here is another way to make ‘unset’ work with session variables from within a function :
Only This works with register_globals being ‘ON’.
The above will not work with register_globals turned on (will only work outside of a function).
The above will work with register_globals on & inside a function
To clarify what hugo dot dworak at gmail dot com said about unsetting things that aren’t already set:
unsetting a non-existent key within an array does NOT throw an error.
Adding on to what bond at noellebond dot com said, if you want to remove an index from the end of the array, if you use unset, the next index value will still be what it would have been.
but you actually get
Array ( [0] => 1 [4] => 2 [5] => 3 [6] => 4 )
This is since even though the last key is removed, the auto indexing still keeps its previous value.
The only time where this would not seem right is when you remove a value off the end. I guess different people would want it different ways.
Hope this helps someone who may need this for some odd reason, I did.
In addition to what timo dot hummel at 4fb dot de said;
>For the curious: unset also frees memory of the variable used.
>
>It might be possible that the in-memory size of the PHP Interpreter isn’t reduced, but your scripts won’t touch the memory_limit boundary. Memory is reused if you declare new variables.
It might be worth adding that functions apparently don’t free up memory on exit the same way unset does..
Maybe this is common knowledge, but although functions destroys variables on exit, it (apparently) doesn’t help the memory.
So if you use huge variables inside functions, be sure to unset them if you can before returning from the function.
In my case, if I did not unset before return, then the script would use 20 MB more of memory than if I did unset.
This was tested with php 5.0.4 on apache 2 on windows xp, with no memory limit.
Before I did the test, I was under the impression that when you exit from functions, the memory used inside it would be cleared and reused. Maybe this should be made clear in the manual, for either unset() or in the chapter for functions.
Despite much searching, I have not yet found an explanation as to how one can manually free resources from variables, not so much objects, in PHP. I have also seen many comments regarding the merits and demerits of unset() versus setting a variable to null. Thus, here are the results of some benchmarks performed comparing unset() of numerous variables to setting them to null (with regards to memory usage and processing time):
10 variables:
Unset:
Memory Usage: 296
Time Elapsed: 1.0013580322266E-5
Null set:
Memory Usage: 1736
Time Elapsed: 5.9604644775391E-6
50 variables:
Unset:
Memory Usage: 296
Time Elapsed: 3.6001205444336E-5
Null set:
Memory Usage: 8328
Time Elapsed: 3.2901763916016E-5
100 variables:
Unset:
Memory Usage: 296
Time Elapsed: 5.6982040405273E-5
Null set:
Memory Usage: 15928
Time Elapsed: 5.8174133300781E-5
1000 variables:
Unset:
Memory Usage: 296
Time Elapsed: 0.00041294097900391
Null set:
Memory Usage: 168096
Time Elapsed: 0.00067591667175293
10000 variables:
Unset:
Memory Usage: 296
Time Elapsed: 0.0042569637298584
Null set:
Memory Usage: 1650848
Time Elapsed: 0.0076930522918701
100000 variables:
Unset:
Memory Usage: 296
Time Elapsed: 0.042603969573975
Null set:
Memory Usage: 16249080
Time Elapsed: 0.087724924087524
300000 variables:
Unset:
Memory Usage: 296
Time Elapsed: 0.13177299499512
Null set:
Memory Usage: 49796320
Time Elapsed: 0.28617882728577
Perhaps my test code for the null set was flawed, but despite that possibility it is simple to see that unset() has minimal processing time impact, and no apparent memory usage impact (unless the values returned by memory_get_usage() are flawed). If you truly care about the
4 microseconds saved over
In PHP 5.0.4, at least, one CAN unset array elements inside functions from arrays passed by reference to the function.
As implied by the manual, however, one can’t unset the entire array by passing it by reference.
Note that PHP 4 will generate a warning if you try to unset an array index that doesn’t exist and whose parent doesn’t exist.
?>
RESULT: «Notice: Undefined index: Bar»
On PHP5 no error is raised, which seems to me like the correct behaviour.
Note that using unset($foo[‘Bar’]) in the above example does not generate a warning in either version.
(Tested on 4.4.9 and 5.2.4)
Just to confirm, USING UNSET CAN DESTROY AN ENTIRE ARRAY. I couldn’t find reference to this anywhere so I decided to write this.
The documentation is not entirely clear when it comes to static variables. It says:
If a static variable is unset() inside of a function, unset() destroys the variable and all its references.
The above example would output:
And it does! But the variable is NOT deleted, that’s why the value keeps on increasing, otherwise the output would be:
The references are destroyed within the function, this handeling is the same as with global variables, the difference is a static variable is a local variable.
Be carefull using unset and static values as the output may not be what you expect it to be. It appears to be impossible to destroy a static variable. You can only destroy the references within the current executing function, a successive static statement will restore the references.
The documentation would be better if it would say:
«If a static variable is unset() inside of a function, unset() destroys all references to the variable. «
Here’s my variation on the slightly dull unset method. It throws in a bit of 80’s Stallone action spice into the mix. Enjoy!
/**
* function rambo (first blood)
*
* Completely and utterly destroys everything, returning the kill count of victims
*
* @param It don’t matter, it’s Rambo baby
* @return Integer Body count (but any less than 500 and it’s not really worth mentioning)
*/
function rambo () <
// Get the victims and initiate that body count status
$victims = func_get_args ();
$body_count = 0 ;
about unset for arrays
if you unset the last array member
$ar[0]==2
$ar[1]==7
$ar[2]==9
So, unset has no effect to internal array counter.
further I realized that an object, when getting detroyed, does care about destroying variable in object space visibility but not those in local visibility, be aware of the found pattern:
Instead of using the unset function for unregistering your session or other array values you can also do this samll feature and get this task done with just 1 line code.
Suppose, if you like to unregister your session store values.
You can use:
Well this syntax saves lot’s of time instead of unsetting each values.
you may wan’t to unset all variables which are defined, here’s one way:
?>
you can also save than a serialized var of the «memory» and perhaps store this in a temporary file.. very usefull if you work with text files and/or file uploads when you’ve got very large variables.
When unset from an array, if you unset all elements, the array is always set
In regard to some confusion earlier in these notes about what causes unset() to trigger notices when unsetting variables that don’t exist.
Sometimes you need to assigne values to an array index in some loop (if, while, foreach etc.) but you wish to set starting index key to some number greater then zero (lets say 5). One idea how to do this is:
Array ( [5] => 5 [6] => 10 [7] => 15 [8] => 100 )
You can not unset a numeric key of an array, if key is a string. See this example:
And here is the output:
Test: 1
array(3) <
[10]=>
array(1) <
[0]=>
string(6) «apples»
>
[20]=>
array(1) <
[0]=>
string(7) «bananas»
>
[30]=>
array(1) <
[0]=>
string(7) «peaches»
>
>
key: 10
key exists: bool(true)
typeof key is: integer
key: 20
key exists: bool(true)
typeof key is: integer
key: 30
key exists: bool(true)
typeof key is: integer
array(0) <
>
Two ways of unsetting values within an array:
$SomeObj = new TheObj ;
This applied to the «virtual» array variable too, see more at http://bugs.php.net/bug.php?id=33513 (at feedback) about it.
PS: we used PHP version 5.1.0-dev from the CVS snapshot when we wrote the above codes.
1) unsetting of superglobals is done globally, i.e. unsetting inside the function affects GLOBALLY.
foo ();
bar (); //issues E_NOTICE ($GLOBALS not defined)
$GLOBALS = 3 ;
bar (); //displays int(3)
В PHP5 присутствует неоднозначность в работе foreach, которая возникает из-за манипуляции с внутренним указателем массива или модификации элементов массива, который перебирается в foreach.
Вот несколько примеров foreach
1. Неработающий current()
2. unset() может исключить элемент из перебора, а может и нет
В PHP7 реализован более рациональный подход.
Первое, foreach может итерировать:
Второе, foreach может перебирать
Итерация массива по значению
Никогда не использует и меняет внутренний указатель массива.
Не делает копию массива, а лочит оригинальный массив (реф.счетчик++)
Модификации оригинального массива игнорируются
// до PHP 7 result
1
3
Итерация массива по ссылке
В основном повторяет поведение PHP5.
Изменяет внутренний указатель (передвигает его на следующий элемент)
@wtf: и где он его изменяет, нужно уточнение?
Модификация внутреннего указателя функциями next(), reset(), end() не отражаются на указателе foreach,
при следующей итерации foreach восстановит свой указатель (как в PHP5)
Удаление следующего элемента (указателя foreach) удалит его из перебора
Добавление новых элементов после указателя, добавит их в перебор
Добавление новых элементов после указателя (когда мы находимся в конце массива), все равно добавит их в перебор
// hhvm фейлит данный тест с бесконечным циклом
В случае использования нескольких ссылочного foreach по одному и тому же массиву, работают теже правила, независимо друг от друга (не работает в PHP5)
Возможно изменение массива, итерируемого в ссылочном foreach используя функции array_pop(), array_push(), array_shift(), array_unshift()
Эти функции сохраняют указатель массива foreach или передвигают его к следующему элементу, если текущий удален (не работает в PHP5)
Итерация простых объектов по значению
Работает также как итерация массива по ссылке, но используется значение объекта, вместо ссылки, как результат объект может быть изменен, НО не заменен.
Итерация простых объектов по ссылке
Работает также как итерация массива по ссылке
Гость 15:29 04 Апрель, 2016 Документация верна с замечанием:
Пока вы не изменяете итерируемый массив, работа происходит с оригинальным массивом. При попытке изменения массива (в цикле) сработает copy-on-write и будет создана копия.
Гость 13:43 04 Апрель, 2016 Здравствуйте.
Немного не понятно, у Вас написано,
«Итерация массива по значению
Не делает копию массива, а лочит оригинальный массив (реф.счетчик++)»
«foreach by-value operates on a copy of the array ¶
When used in the default by-value mode, foreach will now operate on a copy of the array being iterated rather than the array itself. This means that changes to the array made during iteration will not affect the values that are iterated.»
(Когда используется по значению, foreach теперь будет обрабатывать копию массива, а не сам массив. )
Где правда?