С# - не обезьяна с индексом цикла

Один из элементов контрольного списка Стива Макконнелла состоит в том, что вам не следует обезьяна с индексом цикла (Глава 16, стр. 25, Индексы циклов, PDF формат).

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

В недавнем обзоре кода я нашел этот неудобный цикл и сразу же пометил его как подозреваемого.

        for ( int i=0 ; i < this.MyControl.TabPages.Count ; i++ )
        {
            this.MyControl.TabPages.Remove ( this.MyControl.TabPages[i] );
            i--;
        }

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

Этот цикл мог быть записан как

        while(MyControl.TabPages.Count > 0)
            MyControl.TabPages.RemoveAt(0);

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

        MyControl.TabPages.Clear();

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

Есть ли лучшая артикуляция, почему это плохая практика?

Ответ 1

Я думаю, что твоя артикуляция велика. Возможно, это можно сформулировать так:

Так как логику можно выразить много яснее, он должен.

Ответ 2

Ну, это добавляет путаницу для небольшой цели - вы можете так же легко написать:

while(MyControl.TabPages.Count > 0)
{
    MyControl.TabPages.Remove(MyControl.TabPages[0]);
}

или (проще)

while(MyControl.TabPages.Count > 0)
{
    MyControl.TabPages.RemoveAt(0);
}

или (простейший)

MyControl.TabPages.Clear();

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

Ответ 3

Все о ожидании.

Когда вы используете loopcounter, вы ожидаете, что он увеличивает (уменьшает) каждую итерацию цикла с одинаковой суммой.

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

complexity leads to misunderstanding

misunderstanding leads to bugs

bugs leads to the dark side.

Ответ 4

Я согласен с вашей задачей. Если они хотят сохранить цикл for, код:

for ( int i=0 ; i < this.MyControl.TabPages.Count ; i++ ) {
    this.MyControl.TabPages.Remove ( this.MyControl.TabPages[i] );
    i--;
}

сводится к следующему:

for ( int i=0 ; i < this.MyControl.TabPages.Count ; ) {
    this.MyControl.TabPages.Remove ( this.MyControl.TabPages[i] );
}

а затем:

for ( ; 0 < this.MyControl.TabPages.Count ; ) {
    this.MyControl.TabPages.Remove ( this.MyControl.TabPages[0] );
}

Но цикл while или метод Clear(), если он существует, явно предпочтительнее.

Ответ 5

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

 while (this.MyControl.TabPages.Count>0)
 {
            this.MyControl.TabPages.Remove ( this.MyControl.TabPages[0] );
 }

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

Ответ 6

Это может быть яснее:

while (this.MyControl.TabPages.Count > 0)
{
  this.MyControl.TabPages.Remove ( this.MyControl.TabPages[0] );
}

Ответ 7

Исходный код сильно избыточен, чтобы сгибать действие for-loop на то, что необходимо. Приращение не требуется и сбалансировано декрементом. Это должны быть PRE-инкременты, а не POST-приращения, потому что концептуально пост-инкремент является неправильным. Сравнение со счетчиками табуляции является полу-избыточным, так как это хакерский способ проверить, что контейнер пуст.

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

Ответ 8

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

  i=0;
  while(i < MyControl.Tabpages.Count)
    if (wantToDelete(MyControl.Tabpages(i))
      MyControl.Tabpages.RemoveAt(i);
    else
      i++;
, а не jinxing индекс цикла после каждого удаления. Или, еще лучше, индексируйте индекс вниз, чтобы при удалении элемента он не повлиял на индекс будущих предметов, требующих удаления. Если многие элементы удалены, это также может помочь свести к минимуму количество времени, затрачиваемого на перемещение элементов после каждого удаления.

Ответ 9

Я не уверен, полностью ли понимаю, что вы после, но может быть что-то вроде этого?

for ( int i=this.MyControl.TabPages.Count - 1 ; i >= 0  ; i-- )
{
    this.MyControl.TabPages.Remove ( this.MyControl.TabPages[i] );
}

или просто это из коллекции коллекции TabPages IList:

    this.MyControl.TabPages.Clear();

Ответ 10

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

Ответ 11

Я думаю, указывая на то, что итерации цикла управляются не "i ++", как ожидал любой, но сумасшедшей настройкой "i--" должно было быть достаточно.

Я также считаю, что изменение состояния "i" путем вычисления счета, а затем изменения счетчика в цикле также может привести к потенциальным проблемам. Я ожидал бы, что цикл for, как правило, имеет "фиксированное" количество итераций и единственную часть условия цикла for, которая изменяется как переменная цикла "i".