Что лучше в цикле 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);

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

Ответы

Ответ 1

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

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

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

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

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

Область применения:
Для начала PHP не является действительно блочным облаком, как C (++), С#, Java, Perl или (с удачей) ECMAScript6... Это означает, что переменная $value не будет отменена, как только цикл закончен. При циклировании по ссылке это означает, что ссылка на последнее значение любого объекта/массива, который вы повторяли, плавает. Фраза "авария, ожидающая случиться" должна spring мыслить.
Рассмотрим, что происходит с $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 позаботится об этом для нас), поэтому большую часть времени вы не беспокоитесь. Когда речь идет о ссылках, это источник разочарования, таинственных сообщений об ошибках или дорожных ценностей, где вы используете сложные вложенные циклы, возможно, с несколькими ссылками... Ужас, ужас.
Кроме того, со временем, кто скажет, что следующий человек, работающий над вашим кодом, не будет озадачен 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

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

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

Учитывая этот тест script:

$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

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

Также стоит отметить, что, как было предложено в Elias Van Ootegem Answer, чтобы правильно очистить после себя, вы должны отключить ссылку после завершения цикла. То есть unset($v); И прирост производительности должен быть измерен против потери читаемости.

Ответ 3

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

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

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

Ответ 4

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

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

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

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

    $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
    

Ответ 5

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

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

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

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

Ответ 6

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

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

Ответ 7

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

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

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

Ответ 8

Большинство ответов интерпретировали ваш вопрос о производительности.

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

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

Как вы сказали, оба делают то же самое. Оба работают. В конце концов, лучше всего часто бывает мнение.

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

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

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

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