Pull to refresh

Comments 145

Сам на это натыкался. Кстати, в мануале прямо указано — делайте unset. Я даже в фреймворке yii нашел участок кода где есть эта бага.
Да, многие разработчики на этом деле подрывались, и описание этой проблемы действительно есть в мануале.

Но, я не встречал (около года назад) нормального описания этой проблемы на русском языке, что могло сильно попортить нервы молодым разработчикам. Поэтому, автору респект за описание проблемы в доступной форме.
UFO just landed and posted this here
Не всё, скаляры по значению.
Тьфу, в смысле для скаляров присваивание по значению, а иногда нужна ссылка.
Если эти скаляры длинной меньше длинны указателя, то по значению они будут передаваться быстрее!
В общем не найти мне статьи где это подробно расписано, вот вам простейший тест:
// Начальная переменная со строкой в 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'ом напортачил, префикс работает только когда указан один из ключей 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);
Если честно, то сейчас такие конструкции можно заменить на более короткие.

Про особенность работы ссылок в внутри 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, и негативных побочных эффектов нет.
А можно и так:
$arrayOnlyIntValues = array_map(function($value) {
        return (int) $value;
}, $array);


Можно, конечно, по разному, просто я пытался привести пример использования именно по ключам. С самого начала я сказал, что большинство задач можно решить разными путями, и намеряно привёл путь решения через foreach и ссылки.
Простая задача — экономия памяти. При передачи по ссылке значение переменной не копируется.
При передаче не по ссылке значение так же не копируется =) Только при изменении.
Так-то оно так, но в этом конкретном примере — нет резона использовать foreach по ссылке если не нужно изменять переменную.
А что имеете в виду под управляемыми средами? Не это?
Причем переменные объектного типа и объекты в Java — совершенно разные сущности. Переменные объектного типа являются ссылками, то есть неявными указателями на динамически создаваемые объекты.
или это?
Кроме того, в C# решено было перенести некоторые возможности C++, отсутствовавшие в Java: ... передача параметров в метод по ссылке... Также в C# оставили ограниченную возможность работы с указателями

Конечно давайте будем растить память! Она ведь и так в php не чиститься от мусора, а мы добьем лежачего :)
> не чиститься от мусора

Поясните.
я имел ввиду то что до версии 5.3 в 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 расписано на полторы тысячи строк.
PSR-0 требует vendor name… Наткнулся случайно, так почти целый день убил придумывая…
int aaaaazArray[111]; // Вот он массив моей мечты!11
int a[MAX_INT]; //здоровенный!
#!/usr/bin/python

def infinity():
while True:
yield None

yaaaaaaaaz_array = infinity() #здоооорооовеееееннныыыыыый!!!11
В бывшей фирме я встретился с конвенцией поименования переменных:
l (local), p (private), итд… + a (array), s (string), i (integer), итд… + + название…
Ну и потом количество рогов скота выглядело примерно: $pirog.
Очень сильно помогало понимать, что находится в коде и какого типа данные.

Уже очень давно пользуюсь такой системой именования переменных. Действительно укоряет понимание кода.
И без большой буквы после префикса?
С большой, конечно) Собственно как в венгерский нотации. Видимо с изучения си++ это у меня и осталось.
Использовать 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 — самый опасный язык для работы с указателями, или я что-то упускаю?
Не будьте столь категоричным. В любом языке есть свои нюансы, хорошие и плохие стороны, начинающие и опытные разработчики.
Меня реально заколебали ответы «у дргих еще хуже». И в политике и в повседневной жизни. Как вообще может прийти в голову считать это аргументом?
В php любая переменная — это указатель. При присваивании или передаче, новая переменная становится указателем на те же данные, а у данных увеличивается счетчик ссылок на них. При изменении одной из переменных, для нее создается копия данных. Амперсанд относится не к переменной, а к выполняемой операции (прямого или косвенного присваивания). Он означает, что при изменении переменной-приёмника не будет создаваться копия, а данные будут изменяться в источнике. В этом плане php работает хорошо и никому обычно не мешает. Однако, разработчики, вероятнее всего, изучавшие программирование на примере Си, часто не знают об этой особенноси php, и не учитывают её при составлении скриптов. Так возникают баги, подобные описанному.
Что-то изменилось с тех пор как я разбирался с этим и в мане врут?
По умолчанию, переменные всегда присваиваются по значению. То есть, когда вы присваиваете выражение переменной, все значение оригинального выражения копируется в эту переменную.
Тут про «copy on write», значение (в нутрях php, в zend engine), копирует только когда это действительно необходимо, иже когда оно изменяется, а так если иметь 10к переменных с одинаковым значением (скопировав их с какого-то одного) — память будет потребляться только на сами структуры переменных. И в мане кстати все ок, в мане про это знают :)
Насколько я знаю к скалярным типам это не относится, только массивы и объекты. Нет?
Почему, это в первую очередь к скалярным типам и относится, ну и к массивам, а объекты всегда передаются по ссылке, они с copy on write никак не связаны, при наличии двух ссылок на объект и изменении объекта в одной — копия создана не будет.
Блин да достали вы со своими скалярами :) Они не просто так по значению передаются. Это тоже оптимизация ;)
Там примеры не со скалярными типами.
Как эта особенность проявляется внешне, и как она связана с обсуждаемой проблемой?
В .NET есть указатели, но ими я помню очень редко пользовался, в основном, что бы перейти на нативный уровень, блокировав сборщик мусора. И то там были проблемы — например char в .NET 16 разрядный. Не надо пользоваться в управляемых языках указателями.
foreach ($newitems as $key=>$item) обещали сделать deprecated
Что конкретно из этого? Весь foreach? $key=>$item?
Рекомендуется конструкция foreach ($array => &$item)
Круто, а как же быдлоконструкция if(!$key) echo 'br';?
Это только в планах. Может быть, планы изменились, я плохо следил последнее время. Без $c as $b=>$c ключ нормальными способами не вытащить сейчас.
«ключ нормальными способами не вытащить сейчас»

Иногда, когда мне нужны только ключи и обход по ним, делаю foreach (array_keys($items) as $key) {}
Чем этот способ плох?
А если и то и то?
// Думаю конструкция выше может быть deprecated только в пользу ArrayIterator.
То есть ключи не рекомендуются? В манах что-то ничего не нашёл.
Скорее рекомендуется без ключа. Вот только я не помню, где именно это читал. На официальном сайте php 100%
Предпочитаю всё unset'ить как только отпала надобность, чтоб не переживать.
Мне бы было лень, я тупо не юзаю переменные из-за скобок (только для «передачи» параметров беру из-за скобки), а если надо то практически всегда это отдельная сущность, которая выносится в метод.
Вася Пупкин тоже не переживал :)
Когда пишешь unset для каждой переменной, консольный демон практически не течет, так что очень рекомендую. Тем более в СИ программист сам следит за памятью, что в PHP и подразумевается, но почему то, никто этого не делает.
собственно привычка с С/C++ malloc/free и new/delete должны быть парными и крайне желательно в одном блоке, край — конструктор/деструктор.
unset'ы код загрязняют сильно. В С++ то это легко обходится всякими Locker'ами и RAII, но там область видимости хорошая и строгая.
1. Это не баг, а вполне разумное и ожидаемое поведение.
2. Всегда надо чистить пространство имён с помощью unset, особенно тогда, когда используете передачу по ссылке. Не заботиться о чистке можно только тогда, когда у вас сразу после этого заканчивается область видимости, но и тогда лучше поставить unset на случай последующих доработок.
На самом деле беда в том, что у PHP слишком долгая память переменных… по уму — за блоком foreach $item уже не должна быть определена (что и делается в других, более строгих языках). Хочешь сохранять — делай это явно, вот и вся история ;-)
Только это не долгая память переменных называется, а область видимости %) А так да, в пхп как всегда отличились.
В С++ разве не аналогично? Переменная объявленная в цикле for имеет область видимости до конца функции?
В C вроде как каждый блок {} имеет свою область видимости, то есть если объявить переменную прямо внутри цикла — то она заменит собой переменную объявленную извне, но только внутри цикла.
Вот вот. И это очень правильно! Поэтому в таких языках не нужны костыли в виде unset.
Ну php проектировался с одной главной идеей — он должен быть простым языком, понятным всем, а блочные области видимости это довольно таки сложно для понимания новичка.
P.S. не удивлюсь если лет через 100 на нём разговаривать начнут, вместо теперешнего эсперанто :)
Понятнее и проще? Оо ну не скажи, по мне так это наоборот сложнее и не очевиднее и ведет к веселым багам.
В C вроде вообще переменные объявляются только в контексте программы или функции, да ещё в самом начале функции. А вот в C++ допустимо, например, for(int i=0; i<10; i++) {… } и тут переменная объявлена вне {} цикла.
В C можно объявлять переменные в начале любого блока, а не только функции. А в С99 их можно объявлять в любом месте, так же как и в C++, в т.ч. и условиях.
Далее, конструкция 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;
}
На больших значениях в массиве рискуете серьезно потратится на память, как минимум на объем самой структуры массива.
А разве $items[$key] это не та же ссылка?
Ну в таком случае вероятность ниже написать $item[$key] =… намного ниже чем просто $item
Для любителей поминусовать вот тут очень подробно расписано почему это не ссылка и как происходит потребление памяти.
Вы не поняли. После цикла за скобками останется в силе последние значения $items и $key. Соответственно если написать $items[$key] = «12» то вы перепишете последний элемент массива
Полагаю что всё же наоброт.
Я не говорю про то, как и где можно что-то перезаписать. Как я сказал в этом комментарии, при использовании подхода:
foreach ($items as $key=>$item) {
$items[$key] += 2;
}
увеличивается потребление памяти.
Еще раз: ничего про присвоение переменных за скобками цикла я не говорил.

А то что написали вы — очевидно, правда с уточнением, что остается только переменная $key, которая была записана в последней итерации. Массив $items уже был. Ничего «багнутого» тут нет.
array_walk($arr, function(&$value){
$value+=2;
});

Или так :)
UFO just landed and posted this here
У вас это узкое место в приложении? :)
UFO just landed and posted this here
Я к тому, что перед тем как мерять спички, надо сначала взять в руки профайлер и посмотреть что у вас действитлеьно тормозит. Подозреваю, что в 99.99% метод обхода массива будет влиять на приложение «никак».
UFO just landed and posted this here
Когда-то сталкивались в реальных условиях, конечно был бы функциональный тест, проблему бы заметили, а так проморгали. Вообще "&" где его можно не использовать, лучше не использовать.
тут об этом написано
Вообще zend замудренно оптимизирует все это дело, imho с++ подобное поведение было бы куда более понятным и правильным
даже в Smarty есть этот баг.

{foreach item=param from=$params}
{$param}
{/foreach}

а после завершения {foreach} переменная {$param} остаётся. Если уже раньшще была определена {$param} — она изменяется.
Я тоже так думал пока у меня на Девелопе отрабатывало нормально а на продакшне посыпалось!
foreach($new as $key => $value)
$new[$key] = $value+2;

Так делал с первого дня, не так красиво — зато без багов.
Вот при работе с встроенными функциями array_map действительно хорош, а при вызове пользовательских идут расходы на смену контекста выполнения, для задачи вроде $a += 2 — это имхо неоправданные траты.
он уже давно быстрее и при вывове пользовательских. К тому же, для array_map можно использовать лямбды, которые, в принципе, inline.
Я вам больше скажу. При работе с кастомными массивами (Traversable) вам такой перебор вообще не поможет изменять данные. Надо всегда быть готовым вместо любого массива использовать Traversable, поэтому стоит ограничивать функционалом, который поддерживается как настоящими массивами, так и кастомными. До тех пор, пока команда разработчиков PHP не доведет поддержку кастомных массивов до того же уровня, что и нативных.
в любом случае, спасибо огромное!
Спасибо за статью! Только сейчас столкнулся на аналогичный баг, вспомнил про статью и нашел решение проблемы :)
!!! Важно, данная бага вылезает не на всех версиях ПХП. Недавно при переносе класса с девелоп на продакшн — вылезла эта бага! Причем на девелопе все работало стабильно без нареканий, на продакшне был крайне удивлен. Огромное спасибо за статью, хорошо, что я ее читал и сразу вспомнил про нее, низко кланяюсь. На девелопе и продакшене стоит 5ка.
Sign up to leave a comment.

Articles