Последний элемент в цикле заслуживает отдельного обращения?

При просмотре я иногда сталкиваюсь с такой петлей:

i = begin
while ( i != end ) {    
   // ... do stuff
   if ( i == end-1 (the one-but-last element) ) {
      ... do other stuff
   }
   increment i
}

Тогда я задаю вопрос: ты бы написал это?

i = begin
mid = ( end - begin ) / 2 // (the middle element)
while ( i != end ) {    
   // ... do stuff
   if ( i > mid ) {
      ... do other stuff
   }
   increment i
}

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

i = begin
mid = ( end - begin ) / 2 //(the middle element)
while ( i != mid ) {    
   // ... do stuff
   increment i
}

while ( i != end ) {
   // ... do stuff
   // ... do other stuff
   increment i
}

Теперь я даже видел вопрос о том, как написать if-хорошо, - и мне стало грустно: что-то здесь не так.

Я ошибся? Если так, что хорошего в загромождении тела цикла особыми случаями, о которых вы знаете заранее, во время кодирования?

13 ответов

Решение

@xtofl,

Я согласен с вашей заботой.

Миллион раз я сталкивался с подобной проблемой.

Либо разработчик добавляет специальную обработку для первого или последнего элемента.

В большинстве случаев стоит просто выполнить цикл от элемента startIdx + 1 или до элемента endIdx - 1 или даже разбить один длинный цикл на несколько более коротких циклов.

В очень редких случаях невозможно разделить цикл.

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

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

  1. Эффективность во время выполнения - работает ли скомпилированный код быстрее или будет быстрее, если делать это по-другому?
  2. Возможность сопровождения кода - Легко ли (для другого разработчика) понять, что здесь происходит?

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

Если это быстрее и менее читабельно, или медленнее, но более читабельно, выясните, какой из факторов имеет большее значение в вашем конкретном случае, а затем решите, как выполнить цикл (или не цикл).

Я знаю, что видел это, когда люди пытались объединить элементы массива в разделенную запятыми строку:

for(i=0;i<elements.size;i++) {
   if (i>0) {
     string += ','
   }
   string += elements[i]
}

У вас либо есть это условие if, либо вам нужно снова продублировать строку += line в конце.

Очевидное решение в этом случае

string = elements.join(',')

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

В последнем опубликованном вами фрагменте вы повторяете код для // .... делать вещи.

Имеет смысл сохранять 2 цикла, когда у вас совершенно другой набор операций с другим набором индексов.

i = begin
mid = ( end - begin ) / 2 //(the middle element)
while ( i != mid ) {    
   // ... do stuff
   increment i
}

while ( i != end ) {
   // ... do other stuff
   increment i
}

Это не так, вы все равно хотите сохранить один цикл. Однако факт остается фактом: вы все еще сохраняете (конец - начало) / 2 числа сравнений. Таким образом, все сводится к тому, хотите ли вы, чтобы ваш код выглядел аккуратно или вы хотите сохранить некоторые циклы ЦП. Звонок твой.

Я пришел к выводу, что, когда я помещаю особые случаи в цикл for, я обычно слишком умен для своего блага.

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

Например:

if(items == null)
    return null;

StringBuilder result = new StringBuilder();
if(items.Length != 0)
{
    result.Append(items[0]); // Special case outside loop.
    for(int i = 1; i < items.Length; i++) // Note: we start at element one.
    {
        result.Append(";");
        result.Append(items[i]);
    }
}
return result.ToString();

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

Если вы не анализируете XML-циклы , они должны быть максимально простыми и краткими.

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

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

Я предпочитаю просто исключить элемент из цикла и дать отдельную обработку вне цикла

Например: давайте рассмотрим случай EOF

i = begin
while ( i != end -1 ) {    
   // ... do stuff for element from begn to second last element
   increment i
}

if(given_array(end -1) != ''){
   // do stuff for the EOF element in the array
}

Конечно, вещи в специальном корпусе в петле, которые можно вытащить, глупы. Я бы тоже не дублировал do_stuff; Я бы поместил его в функцию или макрос, чтобы не копировать и не вставлять код.

Какой из них работает лучше?

Если количество элементов очень велико, то я всегда выполняю цикл один раз, особенно если вы собираетесь выполнить какую-либо операцию с каждым элементом. Стоимость оценки условного может быть меньше, чем зацикливание в два раза.

Ой, конечно, вы не зацикливаетесь дважды... В этом случае предпочтительнее использовать два цикла. Тем не менее, я утверждаю, что основное внимание должно уделяться производительности. Нет необходимости вводить условие в цикл (N раз), если вы можете разделить работу простым манипулированием границами цикла (один раз).

Еще одна вещь, которую я не хочу видеть, это шаблон для случая:

for (i=0; i<5; i++)
{
  switch(i)
  {
    case 0:
      // something
      break;
    case 1:
      // something else
      break;
    // etc...
  }
}

Я видел это в реальном коде.

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

Особый случай должен быть выполнен вне цикла, если он должен быть выполнен только один раз.

Тем не менее, может существовать индекс или некоторая другая переменная (переменные), которые просто легче держать в цикле из-за области видимости. Может также существовать контекстная причина для того, чтобы все операции над структурой данных объединялись внутри структуры управления циклами, хотя я думаю, что это слабый аргумент сам по себе.

Другие вопросы по тегам