Что лучше в цикле foreach ... используя символ & или переназначение на основе ключа?

рассмотрим следующий PHP-код:

//Method 1
$array = array(1,2,3,4,5);
foreach($array as $i=>$number){
  $number++;
  $array[$i] = $number;
}
print_r($array);


//Method 2
$array = array(1,2,3,4,5);
foreach($array as &$number){
  $number++;
}
print_r($array);

оба метода выполняют одну и ту же задачу, один путем назначения ссылки, а другой путем повторного назначения на основе ключа. Я хочу использовать хорошие методы программирования в своей работе, и мне интересно, какой метод является лучшей практикой программирования? Или это одна из тех вещей, которые не имеют значения?

8 ответов


поскольку самый высокий балльный ответ утверждает, что второй метод лучше во всех отношениях, я чувствую себя вынужденным опубликовать ответ здесь. Верно, зацикливание по ссылке is более эффективный, но это не без рисков/подводных камней.
Итог, как всегда: "что лучше, X или Y", единственные реальные ответы, которые вы можете получить:

  • это зависит от того, что вы После / что вы делаете
  • о, оба в порядке, если вы знаете, что вы делать
  • X хорошо для такие, Y лучше для так
  • не забывайте о Z, и даже тогда ...( "что лучше X, Y или Z" тот же вопрос, поэтому применяются те же ответы: это зависит, оба в порядке, если...)

как бы то ни было, как показал Orangepill, эталонный подход обеспечивает лучшую производительность. В этом случае компромисс между производительностью и кодом, который менее подвержен ошибкам, проще читать/maintan. В общем, считается, что лучше пойти на более безопасный, надежный и более ремонтопригодный код:

'отладка в два раза сложнее, чем написание кода в первую очередь. Поэтому, если вы пишете код как можно умнее, вы, по определению, недостаточно умны, чтобы отладить его.- Брайан Керниган!--26-->

я думаю, это означает, что первый способ должен считаться лучшие практики. Но это не значит, что второй подход следует избегать в любое время, так что ниже приведены недостатки, подводные камни и причуды, которые вам придется учитывать при использовании ссылки в foreach петли:

объем:
Для начала PHP не является действительно блочным, как C (++), C#, Java, Perl или (с некоторой удачей) ECMAScript6... Это означает, что $value переменная не будет unset как только цикл закончился. При циклировании по ссылке это означает ссылку на последнее значение любого объекта / массива, который вы повторяли, плавает вокруг. Фраза "что-то случиться" должно прийти на ум.
Подумайте, что происходит с $value, а впоследствии $array в следующий код:

$array = range(1,10);
foreach($array as &$value)
{
    $value++;
}
echo json_encode($array);
$value++;
echo json_encode($array);
$value = 'Some random value';
echo json_encode($array);

вывод этого фрагмента будет:

[2,3,4,5,6,7,8,9,10,11]
[2,3,4,5,6,7,8,9,10,12]
[2,3,4,5,6,7,8,9,10,"Some random value"]

другими словами, повторно используя $value переменная (которая ссылается на последний элемент в массиве), вы фактически манипулируете массивом себя. Это делает код подверженным ошибкам и затрудняет отладку. В отличие от:

$array = range(1,10);
$array[] = 'foobar';
foreach($array as $k => $v)
{
    $array[$k]++;//increments foobar, to foobas!
    if ($array[$k] === ($v +1))//$v + 1 yields 1 if $v === 'foobar'
    {//so 'foobas' === 1 => false
        $array[$k] = $v;//restore initial value: foobar
    }
}

ремонтопригодность/идиот-стойкости:
Конечно, вы можете сказать, что болтающаяся ссылка-это простое исправление, и Вы были бы правы:

foreach($array as &$value)
{
    $value++;
}
unset($value);

но после того, как вы написали свои первые 100 циклов со ссылками, вы действительно верите, что не забудете снять одну ссылку? Конечно, нет! Это так необычно для unset переменные, которые были используется в цикле (мы предполагаем, что GC позаботится об этом для нас), поэтому большую часть времени вы не беспокоитесь. Когда ссылки задействованы, это источник разочарования, таинственные сообщения об ошибках или путешествия значений, когда вы используете сложные вложенные циклы, возможно, с несколькими ссылками... Ужас, ужас.
Кроме того, с течением времени, кто может сказать, что следующий человек, работающий над вашим кодом, не будет foget о unset? Кто знает, может, он даже не знает об этом. ссылки, или увидеть ваши многочисленные unset звонки и считают их избыточными, признаком того, что вы параноик, и удалите их все вместе. Комментарии сами по себе вам не помогут: их нужно читать, и все, кто работает с вашим кодом, должны быть тщательно проинформированы, возможно, они прочитайте полную статью на эту тему. Примеры, перечисленные в связанной статье, плохие, но я видел и хуже:

foreach($nestedArr as &$array)
{
    if (count($array)%2 === 0)
    {
        foreach($array as &$value)
        {//pointless, but you get the idea...
            $value = array($value, 'Part of even-length array');
        }
        //$value now references the last index of $array
    }
    else
    {
        $value = array_pop($array);//assigns new value to var that might be a reference!
        $value = is_numeric($value) ? $value/2 : null;
        array_push($array, $value);//congrats, X-references ==> traveling value!
    }
}

это простой пример проблемы стоимости путешествия. Я не сделать это, кстати, я наткнулся на код, который сводится к следующему... честно. Помимо обнаружения ошибки и понимания кода (что было затруднено ссылками), это все еще довольно очевидно в этом примере, главным образом потому, что это всего лишь 15 строк, даже используя просторный стиль кодирования Allman... Теперь представьте, что эта базовая конструкция используется в коде, который на самом деле тут что-то еще немного более сложное и значимое. Удачной отладки удачи что.

побочные эффекты:
Часто говорят, что функции не должны иметь побочных эффектов, потому что побочные эффекты (по праву) считается код-запах. Хотя foreach является языковой конструкцией, а не функцией, в вашем примере должно применяться то же самое мышление. Когда вы используете слишком много ссылок, вы слишком умны для своего же блага и можете обнаружить, что вам нужно пройти через цикл, просто чтобы знать, на что ссылается то, что переменная, и когда.
Первый метод не имеет этой проблемы: у вас есть ключ, поэтому вы знаете, где вы находитесь в массиве. Более того, с помощью первого метода вы можете выполнить любое количество операций над значением, не изменяя исходное значение в массиве (отсутствие побочных эффектов):

function recursiveFunc($n, $max = 10)
{
    if (--$max)
    {
        return $n === 1 ? 10-$max : recursiveFunc($n%2 ? ($n*3)+1 : $n/2, $max);
    }
    return null;
}
$array = range(10,20);
foreach($array as $k => $v)
{
    $v = recursiveFunc($v);//reassigning $v here
    if ($v !== null)
    {
        $array[$k] = $v;//only now, will the actual array change
    }
}
echo json_encode($array);

это генерирует вывод:

[7,11,12,13,14,15,5,17,18,19,8]
$array = range(10,20);
foreach($array as &$v)
{
    $v = recursiveFunc($v);//Changes the original array...
    //granted, if your version permits it, you'd probably do:
    $v = recursiveFunc($v) ?: $v;
}
echo json_encode($array);
//[7,null,null,null,null,null,5,null,null,null,8]

чтобы противостоять этому, нам придется либо создать временную переменную, либо вызвать функцию tiwce, либо добавить ключ и пересчитать начальное значение $v, но это просто глупо (это добавляет сложность, чтобы исправить то, что не должно быть сломано):

foreach($array as &$v)
{
    $temp = recursiveFunc($v);//creating copy here, anyway
    $v = $temp ? $temp : $v;//assignment doesn't require the lookup, though
}
//or:
foreach($array as &$v)
{
    $v = recursiveFunc($v) ? recursiveFunc($v) : $v;//2 calls === twice the overhead!
}
//or
$base = reset($array);//get the base value
foreach($array as $k => &$v)
{//silly combine both methods to fix what needn't be a problem to begin with
    $v = recursiveFunc($v);
    if ($v === 0)
    {
        $v = $base + $k;
    }
}

в любом случае, добавление ветвей, временных переменных и что у вас есть, скорее поражает точка. Во-первых, он вводит дополнительные накладные расходы, которые съедят в ссылках на преимущества производительности, которые вы дали в первую очередь.
Если вам нужно добавить логику в цикл, чтобы исправить то, что не должно нуждаться в исправлении, вы должны отступить назад и подумать о том, какие инструменты вы используете. 9/10 раз, вы выбрали неправильный инструмент для работы.

последнее, что, по крайней мере для меня, является убедительным аргументом для первого метода, просто: читабельности. Оператор ссылки (&) легко упускается из виду, если вы делаете некоторые быстрые исправления или пытаетесь добавить функциональность. Вы можете создавать ошибки в коде, который работает нормально. Более того: поскольку он работал нормально, вы не можете проверить существующую функциональность так тщательно , потому что не было никаких известных проблем.
Обнаружение ошибки, которая вошла в производство, из-за вашего игнорирования оператора может показаться глупым, но вы не будете первым, кто столкнулся с этим.

Примечание:
Передача по ссылке во время вызова была удалена с 5.4. Устаньте от функций / функций, которые могут быть изменены. стандартная итерация массива не менялась годами. Я думаю, это то, что вы могли бы назвать "проверенные технологии". Он делает то, что говорит на олове, и это более безопасный способ делать вещи. Что с того, что он медленнее? Если скорость является проблемой, вы можете оптимизировать свой код и ввести ссылки на тогда твои петли.
При написании нового кода перейдите к удобному для чтения, наиболее надежному варианту. Оптимизация может (и действительно должны) подождите, пока все не будет проверено.

и как всегда: преждевременная оптимизация-корень всего зла. И выбрать правильный инструмент для работы, а не потому что он новый и блестящий.


что касается производительности, Метод 2 лучше, особенно если у вас есть большой массив и/или вы используете строковые ключи.

хотя оба метода используют одинаковый объем памяти, первый метод требует поиска массива, даже если этот поиск выполняется индексом, поиск имеет некоторые накладные расходы.

учитывая этот тестовый скрипт:

$array = range(1, 1000000);

$start = microtime(true);
foreach($array as $k => $v){
    $array[$k] = $v+1;
}
echo "Method 1: ".((microtime(true)-$start));

echo "\n";

$start = microtime(true);
foreach($array as $k => &$v){
    $v+=1;
}
echo "Method 2: ".((microtime(true)-$start));

средний вывод

Method 1: 0.72429609298706
Method 2: 0.22671484947205

если я уменьшу тест только до десяти раз вместо 1 миллиона я получаю такие результаты, как

Method 1: 3.504753112793E-5
Method 2: 1.2874603271484E-5

со строковыми ключами разница в производительности будет более выраженным. Так бегут.

$array = array();
for($x = 0; $x<1000000; $x++){
    $array["num".$x] = $x+1;
}

$start = microtime(true);
foreach($array as $k => $v){
    $array[$k] = $v+1;
}
echo "Method 1: ".((microtime(true)-$start));

echo "\n";

$start = microtime(true);
foreach($array as $k => &$v){
    $v+=1;
}
echo "Method 2: ".((microtime(true)-$start));

дает производительность, как

Method 1: 0.90371179580688
Method 2: 0.2799870967865

это потому, что поиск по строковому ключу имеет больше накладных расходов, чем индекс массива.

также стоит отметить, что, как предложено в ответ Элиаса Ван Оотегема чтобы правильно очистить после себя, вы должны отключить ссылку после того, как цикл завершенный. Т. е. unset($v); и увеличения представления должны быть измерены против потери в удобочитаемости.


есть некоторые незначительные различия в производительности, но они не будут иметь никакого существенного эффекта.

Я бы выбрал первый вариант по двум причинам:

  1. Это более читабельным. Это немного личное предпочтение, но на первый взгляд, это не сразу очевидно для меня, что $number++ обновление массива. Явно используя что-то вроде $array[$i]++, это намного яснее и менее вероятно вызвать путаницу, когда вы вернетесь к этому коду в год.

  2. Это не оставляет вас с висящей ссылкой на последний элемент в массиве. Рассмотрим этот код:

    $array = array(1,2,3,4,5);
    foreach($array as &$number){
        $number++;
    }
    
    // ... some time later in an unrelated section of code
    $number = intval("100");
    
    // now unexpectedly, $array[4] == 100 instead of 6
    

Я думаю, что зависит. Вы больше заботитесь о удобочитаемости/ремонтопригодности кода или минимизации использования памяти. Второй метод будет использовать немного меньше памяти, но я бы честно предпочел первое использование, поскольку назначенное по ссылке в определении foreach не кажется обычной практикой в PHP.

лично если бы я хотел изменить массив на месте, я бы пошел с третьим вариантом:

array_walk($array, function(&$value) {
    $value++;
});  

первый метод будет незначительно медленнее, потому что каждый раз, когда он будет проходить через цикл, он будет присваивать новое значение переменной $number. Второй метод использует переменную напрямую, поэтому ему не нужно назначать новое значение для каждого цикла.

но, как я уже сказал, разница несущественна, главное, чтобы рассмотреть читаемость.

на мой взгляд, первый метод имеет больше смысла, когда вам не нужно изменять значение в цикле, $ number переменная будет только считываться.

второй метод имеет больше смысла, когда вам нужно часто изменять переменную $number, так как вам не нужно повторять ключ каждый раз, когда вы хотите его изменить, и он более удобочитаем.


вы рассматривали array_map? Он предназначен для изменения значений внутри массива.

$array = array(1,2,3,4,5);
$new = array_map(function($number){
  return $number++ ;
}, $array) ;
var_dump($new) ;

Я бы выбрал #2, но это личное предпочтение.

Я не согласен с другими ответами, использование ссылок на элементы массива в циклах foreach довольно распространено, но это зависит от используемой вами структуры. Как всегда, старайтесь следовать существующим соглашениям о кодировании в вашем проекте или фреймворке.

Я также не согласен с другими ответами, которые предлагают array_map или array_walk. Они вводят накладные расходы вызова функции для каждого элемента массива. Для небольших массивов это не будет будет значительным, но для больших массивов это добавит значительные накладные расходы для такой простой функции. Однако они подходят, если вы выполняете более значительные вычисления или действия - вам нужно решить, что использовать в зависимости от сценария, возможно, путем бенчмаркинга.


большинство ответов интерпретировали ваш вопрос, чтобы быть о производительность.

Это не то, что вы просили. То, что вы спросили:

интересно, какой метод является лучшей практикой программирования?

Как вы сказали, Как сделать то же самое. Оба!--2-->работа. В конце концов, лучше часто это вопрос мнения.

или это один из тех, что на самом деле не имеет значения вещи?

Я бы не зашел так далеко, чтобы сказать это не важно. Как вы можете видеть, может быть производительности для метода 1 и ссылка gotchas для Метода 2.

Я могу сказать, что вопросы больше - это читаемость и логичность. Хотя есть десятки способов увеличить элементы массива в PHP, некоторые из них выглядят как линейный шум или кодовый гольф.

обеспечение того, чтобы ваш код читался в будущем разработчики и вы последовательно применяете свой метод решения проблем гораздо лучше макрос практика программирования на чем угодно микро отличия foreach код.