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

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

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

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

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

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

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

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

 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. Они вносят накладные расходы на вызов функции для каждого элемента массива. Для небольших массивов это не будет значительным, но для больших массивов это добавит значительные накладные расходы для такой простой функции. Тем не менее, они подходят, если вы выполняете более значительные вычисления или действия – вам нужно будет решить, какой из них использовать в зависимости от сценария, возможно, путем бенчмаркинга.

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

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

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

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

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

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

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

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