Комментарии 145
Сам на это натыкался. Кстати, в мануале прямо указано — делайте unset. Я даже в фреймворке yii нашел участок кода где есть эта бага.
Да, многие разработчики на этом деле подрывались, и описание этой проблемы действительно есть в мануале.
Но, я не встречал (около года назад) нормального описания этой проблемы на русском языке, что могло сильно попортить нервы молодым разработчикам. Поэтому, автору респект за описание проблемы в доступной форме.
Но, я не встречал (около года назад) нормального описания этой проблемы на русском языке, что могло сильно попортить нервы молодым разработчикам. Поэтому, автору респект за описание проблемы в доступной форме.
Не всё, скаляры по значению.
Тьфу, в смысле для скаляров присваивание по значению, а иногда нужна ссылка.
Если эти скаляры длинной меньше длинны указателя, то по значению они будут передаваться быстрее!
В общем не найти мне статьи где это подробно расписано, вот вам простейший тест:
// Начальная переменная со строкой в 1000 символов
$a0 = str_repeat('*', 1000);
// Создаем 1000 переменных
extract(range(1, 1000), EXTR_OVERWRITE, 'a');
// Исходное потребление памяти
echo memory_get_usage() . '<br />';
// Копируем по значению
for($i = 1; $i < 1000; $i++) {
${'a' . $i} = $a0;
}
// Память остается прежней
echo memory_get_usage() . '<br />';
// Создаем ту-же строку с нуля
for($i = 1; $i < 1000; $i++) {
${'a' . $i} = str_repeat('*', 1000);
}
// Потребление памяти увеличивается в разы
echo memory_get_usage() . '<br />';
Похоже ваша правда, погонял даже на числах, чтоб не было соблазна написать, что строка это массив символов :)
Правда тест ваш у меня чуть-чуть по другому вёл. extract ввобще объём памяти не изменил (добавил строчку с mem_get_usage и перед ним), а там где «память остаётся прежней» объём вырос, судя по всему на размещение памяти на сами переменные.
Правда тест ваш у меня чуть-чуть по другому вёл. extract ввобще объём памяти не изменил (добавил строчку с mem_get_usage и перед ним), а там где «память остаётся прежней» объём вырос, судя по всему на размещение памяти на сами переменные.
Да это я с extract'ом напортачил, префикс работает только когда указан один из ключей EXTR_PREFIX_*, ну и после префикса еще подчеркивание, соот-но правильно будет так:
$a_0 = str_repeat('*', 1000);
extract(array_combine(range(1, 1000), array_fill(1, 1000, null)), EXTR_PREFIX_ALL, 'a');
echo memory_get_usage() . '<br />';
for($i = 1; $i < 1000; $i++) {
${'a_' . $i} = $a_0;
}
echo memory_get_usage() . '<br />';
for($i = 1; $i < 1000; $i++) {
${'a_' . $i} = str_repeat('*', 1000);
}
echo memory_get_usage() . '<br />';
Какой знакомый код. Вот тут это писал.
Всё всегда передаётся по ссылке, но в случае скаляров, при их изменении, PHP делает копию и изменяет именно её.
Эм… ну я даже затрудняюсь ответить… по назначению? :)
В представленном случае очевидно, зачем — чтобы модифицировать элементы массива путем записи в $item.
Тоже наталкивался на эту проблему, для себя взял за правило всегда использовать перебор $key => $value, а изменять используя $items[$key] =…
Сравнивать этот способ со ссылками по производительности — это как экономить на спичках.
С точки зрения читаемости — код чуть длиннее, согласен, но не критично. Зато надёжно!
Сравнивать этот способ со ссылками по производительности — это как экономить на спичках.
С точки зрения читаемости — код чуть длиннее, согласен, но не критично. Зато надёжно!
Была, а не есть.
Опять 25.
Вообще-то об этой особенности в мануале написано.
Вообще-то об этой особенности в мануале написано.
Никогда такой конструкцией не пользовался, хоть и пишу достаточно сложный проект, я даже не представляю задач где можно пользоваться присвоением по ссылке. И вообще ссылками в управляемых средах не работаем. Обзор интересен.
Придумал. Может быть для оптимизации какой-нибудь?
Как бы проще объяснить… Это тоже самое, что и этот код:
Заменить на
Сделать можно и так, и так. Однако под каждую задачу есть свои, более оптимальные, конструкции.
switch ($var)
{
case 1:
# some code
break;
case 2:
# one more
break;
case 3:
default:
# code block
break;
}
Заменить на
if ($var == 1)
{
# some code
}
elseif ($var == 2)
{
# one more
}
else
{
# code block
}
Сделать можно и так, и так. Однако под каждую задачу есть свои, более оптимальные, конструкции.
Да это понятно. И приведенной вами конструкцией я пользуюсь сразу двумя, так как case не всегда позволяет выполнять требуемые задачи. Но вот с этой ситуацией не очевидно.
Пожалуйста объясните сложнее.
Пожалуйста объясните сложнее.
Понятно. Для переборки индексных массивов, включая разряженные.
Мне кажется лучше пользоваться кейвалью, будет очевидней код, что бы он был более унифицирован, а то от оного апперсанда зависит как работает то что внутри.
Мне кажется лучше пользоваться кейвалью, будет очевидней код, что бы он был более унифицирован, а то от оного апперсанда зависит как работает то что внутри.
$array = array('string', 1, '4', 8, 'second', 'word', 12, '42');
function valuesToInt(array $array = array())
{
foreach ($array as &$value)
{
if (!is_int($value))
{
$value = (int) $value;
}
}
return $array;
}
$arrayOnlyIntValues = valuesToInt($array);
Дай допишу
unset($value);
Если честно, то сейчас такие конструкции можно заменить на более короткие.
Про особенность работы ссылок в внутри foreach я знаю, но не знаю где это действительно можно применить.
Можно более конкретный и полезный пример?
Про особенность работы ссылок в внутри foreach я знаю, но не знаю где это действительно можно применить.
Можно более конкретный и полезный пример?
То же самое можно ещё и так:
Или, для этого конкретного случая, так:
Или сразу на месте:
В этих случаях и читается код так же легко как с foreach, и негативных побочных эффектов нет.
$arrayOnlyIntValues = array_map(function($value) {
if (is_int($value)) {
return $value;
} else {
return (int) $value;
}
}, $array);
Или, для этого конкретного случая, так:
$arrayOnlyIntValues = array_map("intval", $array);
Или сразу на месте:
array_walk($array, function(&$value) {
$value = intval($value);
});
// $array === $arrayOnlyIntValues
В этих случаях и читается код так же легко как с foreach, и негативных побочных эффектов нет.
А можно и так:
Можно, конечно, по разному, просто я пытался привести пример использования именно по ключам. С самого начала я сказал, что большинство задач можно решить разными путями, и намеряно привёл путь решения через foreach и ссылки.
$arrayOnlyIntValues = array_map(function($value) {
return (int) $value;
}, $array);
Можно, конечно, по разному, просто я пытался привести пример использования именно по ключам. С самого начала я сказал, что большинство задач можно решить разными путями, и намеряно привёл путь решения через foreach и ссылки.
Простая задача — экономия памяти. При передачи по ссылке значение переменной не копируется.
А что имеете в виду под управляемыми средами? Не это?
или это?
Причем переменные объектного типа и объекты в Java — совершенно разные сущности. Переменные объектного типа являются ссылками, то есть неявными указателями на динамически создаваемые объекты.
или это?
Кроме того, в C# решено было перенести некоторые возможности C++, отсутствовавшие в Java: ... передача параметров в метод по ссылке... Также в C# оставили ограниченную возможность работы с указателями
Конечно давайте будем растить память! Она ведь и так в php не чиститься от мусора, а мы добьем лежачего :)
> не чиститься от мусора
Поясните.
Поясните.
я имел ввиду то что до версии 5.3 в php не было как такого сборщика мусора, а используется подсчёт ссылок, что приводит к утечки памяти при «циклических ссылках».
то-есть:
Однако в PHP 5.3 боле умный сборщик мусора, но срабатывает он только при наполнении буфера ссылок ru.php.net/manual/en/features.gc.performance-considerations.php
тем самым частичка памяти все же утекает(
а что касаетсяКонечно давайте будем растить память! это был сарказм.
то-есть:
$a = 10; // выделяем область в памяти, одна ссылка
$b = $a; // две ссылки
$b = 1; // выделяем вторую область в памяти под значение 1, одна ссылка на 1, одна ссылка на 10
Однако в PHP 5.3 боле умный сборщик мусора, но срабатывает он только при наполнении буфера ссылок ru.php.net/manual/en/features.gc.performance-considerations.php
тем самым частичка памяти все же утекает(
а что касается
Видимо, правду говорят, что есть две категории людей — которые понимают указатели и которые не понимают указатели.
Это точно, хотя в PHP они в общем-то простые как 2 + 2. Однако конечно умение ими пользоваться иногда позволяет делать вещи намного эффективнее (но не факт, что понятнее).
Очень важна понятность кода, даже в ущерб производительности, ведь почему люди на ассемблере перестали писать? Почему потом поняли, что лучше функциональное, потом ООП, потом сборка мусора, все для удобства. Люди пользуйтесь указателями в исключительных случаях.
У меня была одна ситуация, когда мне пришлось использовать ссылки — обрабатывалась древовидная структура с неограниченной вложенностью и приводилась к двумерному массиву, причём там ещё были какие-то дополнительные условия, которые использование рекурсии делали невозможным. В общем этот кусок кода разросся бы до неприличных размеров, а ссылками уложился в 30-40 строк.
Подробностей не скажу, давно было. Но реально выручило знание ссылок.
Подробностей не скажу, давно было. Но реально выручило знание ссылок.
Понимание указателей не причем. Для любой переменной справедливо, что если мы её что-то присвоили, то она и будет это содержать, не имеет значения, что в ней было до этого и была ли она до этого объявлена. А тут появляется Вася, который выше твоего кода, не посмотрев на всю портянку, дописал одну строчку, используя то же название переменной. В PHP с указателями очень тяжело.
:[|||]:
Боян несусветной давности :)
Поэтому мои переменные всегда имеют вид: yazArray, yazVar, yazItem etc. (:
Страхи какие
$yaPeremenko, $yaObyekteg, $yaMassivcheg…
Знал я одного программиста которы на вопрос «почему для всех переменных ты делаешь префикс?» сказал: «Это мой фирменный знак, моя фишка».
Ну если программист хороший — то почему нет, ради действительно хорошего программиста не жалко будет и кодовые соглашения переписать :)
Да щас! Если программист хороший, он не будет изобретать подобные бредовые «фирменные знаки», не несущие никакого смысла. В нормальном коде не место школьным понтам.
О такие вещи реально глаза ломаются.
По-настоящему хороший программист напишет нормальные имена функций, переменных и т.д., а «фирменные префиксы» — это фирменные понты. Видел я таких: перед тем как насрать в код затирают копирайты настоящего автора и меняют толковые имена на фирменную ерунду. Пару раз натыкался на такое — у одного «VasylFunction», у другого «OlegFunction». Убил бы обоих. Случаи, когда это можно простить — древние CMS-ки, в которых всё в куче и условие if-else расписано на полторы тысячи строк.
По-настоящему хороший программист напишет нормальные имена функций, переменных и т.д., а «фирменные префиксы» — это фирменные понты. Видел я таких: перед тем как насрать в код затирают копирайты настоящего автора и меняют толковые имена на фирменную ерунду. Пару раз натыкался на такое — у одного «VasylFunction», у другого «OlegFunction». Убил бы обоих. Случаи, когда это можно простить — древние CMS-ки, в которых всё в куче и условие if-else расписано на полторы тысячи строк.
хорошо что не yaaaaazArray, yaaaaaazVar…
В бывшей фирме я встретился с конвенцией поименования переменных:
l (local), p (private), итд… + a (array), s (string), i (integer), итд… + + название…
Ну и потом количество рогов скота выглядело примерно: $pirog.
Очень сильно помогало понимать, что находится в коде и какого типа данные.
l (local), p (private), итд… + a (array), s (string), i (integer), итд… + + название…
Ну и потом количество рогов скота выглядело примерно: $pirog.
Очень сильно помогало понимать, что находится в коде и какого типа данные.
Это называется венгерская нотация.
При наличии современных IDE с code completion смысла в её использовании лично я не вижу.
При наличии современных IDE с code completion смысла в её использовании лично я не вижу.
Уже очень давно пользуюсь такой системой именования переменных. Действительно укоряет понимание кода.
Так давно известно же
Использовать var_dump, вместо print_r, и обращать внимание на амперсанд. Чтобы дампить в файл, а не в браузер, альтернативой print_r($var,true) будет такая конструкция:
Есть функция var_export, делает как раз то что вы хотите
var_export($var,true)
Второй параметр аналогичен параметру в print_r возвращает содержимое а не печатает в буфер.
Единственное отличие, принимает только один параметр, когда var_dump принимает бесконечное кол-во параметров
Функция var_export нужна для преобразования структуры данных в php-код, формирующий эти данные. Она может быть полезна, например, при обработке конфигов. В var_dump же можно увидеть тип данных и тот самый амперсанд, означающий, что данные используются кем-то еще.
В print_r тоже второй необязательный параметр, который указывает возвращать или выводить данные.
Как много людей указывают на то, что это давно известно, да еще и не раз обсуждалось. Ну раз еще раз всплыло, значит может быть действительно проблема есть?
В каком еще языке вы не можете знать, что произойдет с переменной, когда вы присвоите ей значение?
В Си во-первых, все переменные нужно объявлять, поэтому вздумай вы использовать переменную item в качестве указателя вы пойдете её объявить и увидите, что такая уже есть. Во-вторых там даже положив в указатель число и проведя арифметические операции, его можно безопасно извлечь, потому что для доступа к тому, куда указывает указатель используется специальный синтаксис.
В питоне и яваскрипте указателей вообще нет.
Php — самый опасный язык для работы с указателями, или я что-то упускаю?
В каком еще языке вы не можете знать, что произойдет с переменной, когда вы присвоите ей значение?
В Си во-первых, все переменные нужно объявлять, поэтому вздумай вы использовать переменную item в качестве указателя вы пойдете её объявить и увидите, что такая уже есть. Во-вторых там даже положив в указатель число и проведя арифметические операции, его можно безопасно извлечь, потому что для доступа к тому, куда указывает указатель используется специальный синтаксис.
В питоне и яваскрипте указателей вообще нет.
Php — самый опасный язык для работы с указателями, или я что-то упускаю?
Не будьте столь категоричным. В любом языке есть свои нюансы, хорошие и плохие стороны, начинающие и опытные разработчики.
Меня реально заколебали ответы «у дргих еще хуже». И в политике и в повседневной жизни. Как вообще может прийти в голову считать это аргументом?
В php любая переменная — это указатель. При присваивании или передаче, новая переменная становится указателем на те же данные, а у данных увеличивается счетчик ссылок на них. При изменении одной из переменных, для нее создается копия данных. Амперсанд относится не к переменной, а к выполняемой операции (прямого или косвенного присваивания). Он означает, что при изменении переменной-приёмника не будет создаваться копия, а данные будут изменяться в источнике. В этом плане php работает хорошо и никому обычно не мешает. Однако, разработчики, вероятнее всего, изучавшие программирование на примере Си, часто не знают об этой особенноси php, и не учитывают её при составлении скриптов. Так возникают баги, подобные описанному.
Что-то изменилось с тех пор как я разбирался с этим и в мане врут?
По умолчанию, переменные всегда присваиваются по значению. То есть, когда вы присваиваете выражение переменной, все значение оригинального выражения копируется в эту переменную.
Тут про «copy on write», значение (в нутрях php, в zend engine), копирует только когда это действительно необходимо, иже когда оно изменяется, а так если иметь 10к переменных с одинаковым значением (скопировав их с какого-то одного) — память будет потребляться только на сами структуры переменных. И в мане кстати все ок, в мане про это знают :)
Насколько я знаю к скалярным типам это не относится, только массивы и объекты. Нет?
Почему, это в первую очередь к скалярным типам и относится, ну и к массивам, а объекты всегда передаются по ссылке, они с copy on write никак не связаны, при наличии двух ссылок на объект и изменении объекта в одной — копия создана не будет.
Блин да достали вы со своими скалярами :) Они не просто так по значению передаются. Это тоже оптимизация ;)
Отписался по поводу скаляров выше: habrahabr.ru/blogs/php/136835/?reply_to=4554710#comment_4554800
На хабре есть хорошая статья про это.
Я бы дал ссылку на www.php.net/manual/ru/features.gc.refcounting-basics.php
Там все хорошо написано.
Там все хорошо написано.
Как эта особенность проявляется внешне, и как она связана с обсуждаемой проблемой?
В .NET есть указатели, но ими я помню очень редко пользовался, в основном, что бы перейти на нативный уровень, блокировав сборщик мусора. И то там были проблемы — например char в .NET 16 разрядный. Не надо пользоваться в управляемых языках указателями.
foreach ($newitems as $key=>$item)
обещали сделать deprecated Что конкретно из этого? Весь foreach? $key=>$item?
Рекомендуется конструкция
foreach ($array => &$item)
вместо => as конечно же.
Круто, а как же быдлоконструкция if(!$key) echo 'br';?
Это только в планах. Может быть, планы изменились, я плохо следил последнее время. Без $c as $b=>$c ключ нормальными способами не вытащить сейчас.
«ключ нормальными способами не вытащить сейчас»
Иногда, когда мне нужны только ключи и обход по ним, делаю foreach (array_keys($items) as $key) {}
Чем этот способ плох?
Иногда, когда мне нужны только ключи и обход по ним, делаю foreach (array_keys($items) as $key) {}
Чем этот способ плох?
А если и то и то?
// Думаю конструкция выше может быть deprecated только в пользу ArrayIterator.
// Думаю конструкция выше может быть deprecated только в пользу ArrayIterator.
То есть ключи не рекомендуются? В манах что-то ничего не нашёл.
Предпочитаю всё unset'ить как только отпала надобность, чтоб не переживать.
Мне бы было лень, я тупо не юзаю переменные из-за скобок (только для «передачи» параметров беру из-за скобки), а если надо то практически всегда это отдельная сущность, которая выносится в метод.
Когда пишешь unset для каждой переменной, консольный демон практически не течет, так что очень рекомендую. Тем более в СИ программист сам следит за памятью, что в PHP и подразумевается, но почему то, никто этого не делает.
1. Это не баг, а вполне разумное и ожидаемое поведение.
2. Всегда надо чистить пространство имён с помощью unset, особенно тогда, когда используете передачу по ссылке. Не заботиться о чистке можно только тогда, когда у вас сразу после этого заканчивается область видимости, но и тогда лучше поставить unset на случай последующих доработок.
2. Всегда надо чистить пространство имён с помощью unset, особенно тогда, когда используете передачу по ссылке. Не заботиться о чистке можно только тогда, когда у вас сразу после этого заканчивается область видимости, но и тогда лучше поставить unset на случай последующих доработок.
На самом деле беда в том, что у PHP слишком долгая память переменных… по уму — за блоком foreach $item уже не должна быть определена (что и делается в других, более строгих языках). Хочешь сохранять — делай это явно, вот и вся история ;-)
Только это не долгая память переменных называется, а область видимости %) А так да, в пхп как всегда отличились.
В С++ разве не аналогично? Переменная объявленная в цикле for имеет область видимости до конца функции?
В C вроде как каждый блок {} имеет свою область видимости, то есть если объявить переменную прямо внутри цикла — то она заменит собой переменную объявленную извне, но только внутри цикла.
Вот вот. И это очень правильно! Поэтому в таких языках не нужны костыли в виде unset.
Ну php проектировался с одной главной идеей — он должен быть простым языком, понятным всем, а блочные области видимости это довольно таки сложно для понимания новичка.
P.S. не удивлюсь если лет через 100 на нём разговаривать начнут, вместо теперешнего эсперанто :)
P.S. не удивлюсь если лет через 100 на нём разговаривать начнут, вместо теперешнего эсперанто :)
В C вроде вообще переменные объявляются только в контексте программы или функции, да ещё в самом начале функции. А вот в C++ допустимо, например, for(int i=0; i<10; i++) {… } и тут переменная объявлена вне {} цикла.
В C можно объявлять переменные в начале любого блока, а не только функции. А в С99 их можно объявлять в любом месте, так же как и в C++, в т.ч. и условиях.
Далее, конструкция for(int i=0; i<10; i++) {… } все же объявляет i только в пределах цикла.
Далее, конструкция for(int i=0; i<10; i++) {… } все же объявляет i только в пределах цикла.
В C переменные можно объявлять в начале абсолютно любого блока, будь то тело функции, тело цикла или просто блок внутри другого блока (очень удобный лайфхак, когда нужна временная переменная, однако не хочется выносить ее в начало функции)…
В С++ не силен, но в том же компиляторе от microsoft есть возможность выбора поведения. А по этой ссылке: docs.freebsd.org/info/g++FAQ/g++FAQ.info.for_scope.html написано что в gcc теперь имеет область до конца цикла.
Поэтому надо делать всегда так
foreach ($items as $key=>$item) {
$items[$key] += 2;
}
foreach ($items as $key=>$item) {
$items[$key] += 2;
}
На больших значениях в массиве рискуете серьезно потратится на память, как минимум на объем самой структуры массива.
А разве $items[$key] это не та же ссылка?
Нет.
Ну в таком случае вероятность ниже написать $item[$key] =… намного ниже чем просто $item
Вы не поняли. После цикла за скобками останется в силе последние значения $items и $key. Соответственно если написать $items[$key] = «12» то вы перепишете последний элемент массива
Полагаю что всё же наоброт.
Я не говорю про то, как и где можно что-то перезаписать. Как я сказал в этом комментарии, при использовании подхода:
foreach ($items as $key=>$item) {
$items[$key] += 2;
}
увеличивается потребление памяти.
Еще раз: ничего про присвоение переменных за скобками цикла я не говорил.
А то что написали вы — очевидно, правда с уточнением, что остается только переменная $key, которая была записана в последней итерации. Массив $items уже был. Ничего «багнутого» тут нет.
Я не говорю про то, как и где можно что-то перезаписать. Как я сказал в этом комментарии, при использовании подхода:
foreach ($items as $key=>$item) {
$items[$key] += 2;
}
увеличивается потребление памяти.
Еще раз: ничего про присвоение переменных за скобками цикла я не говорил.
А то что написали вы — очевидно, правда с уточнением, что остается только переменная $key, которая была записана в последней итерации. Массив $items уже был. Ничего «багнутого» тут нет.
array_walk($arr, function(&$value){
$value+=2;
});
Или так :)
$value+=2;
});
Или так :)
Когда-то сталкивались в реальных условиях, конечно был бы функциональный тест, проблему бы заметили, а так проморгали. Вообще "&" где его можно не использовать, лучше не использовать.
даже в Smarty есть этот баг.
{foreach item=param from=$params}
{$param}
{/foreach}
а после завершения {foreach} переменная {$param} остаётся. Если уже раньшще была определена {$param} — она изменяется.
{foreach item=param from=$params}
{$param}
{/foreach}
а после завершения {foreach} переменная {$param} остаётся. Если уже раньшще была определена {$param} — она изменяется.
foreach($new as $key => $value)
$new[$key] = $value+2;
Так делал с первого дня, не так красиво — зато без багов.
Используйте чаще array_map
Вот при работе с встроенными функциями array_map действительно хорош, а при вызове пользовательских идут расходы на смену контекста выполнения, для задачи вроде $a += 2 — это имхо неоправданные траты.
Я вам больше скажу. При работе с кастомными массивами (Traversable) вам такой перебор вообще не поможет изменять данные. Надо всегда быть готовым вместо любого массива использовать Traversable, поэтому стоит ограничивать функционалом, который поддерживается как настоящими массивами, так и кастомными. До тех пор, пока команда разработчиков PHP не доведет поддержку кастомных массивов до того же уровня, что и нативных.
в любом случае, спасибо огромное!
Спасибо за статью! Только сейчас столкнулся на аналогичный баг, вспомнил про статью и нашел решение проблемы :)
!!! Важно, данная бага вылезает не на всех версиях ПХП. Недавно при переносе класса с девелоп на продакшн — вылезла эта бага! Причем на девелопе все работало стабильно без нареканий, на продакшне был крайне удивлен. Огромное спасибо за статью, хорошо, что я ее читал и сразу вспомнил про нее, низко кланяюсь. На девелопе и продакшене стоит 5ка.
Зарегистрируйтесь на Хабре, чтобы оставить комментарий
Подводный камень в foreach($items as &$item)