Руководство по стилю программирования и конструированию по
Скачать 7.6 Mb.
|
ГЛАВА 16 Циклы 365 Пример цикла foreach (C#) int [] fibonacciSequence = new int [] { 0, 1, 1, 2, 3, 5, 8, 13, 21, 34 }; int oddFibonacciNumbers = 0; int evenFibonacciNumbers = 0; // Подсчитываем количество четных и нечетных чисел в последовательности Фибоначчи. foreach ( int fibonacciNumber in fibonacciSequence ) { if ( fibonacciNumber % 2 ) == 0 ) { evenFibonacciNumbers++; } else { oddFibonacciNumbers++; } } Console.WriteLine( “Found {0} odd numbers and {1} even numbers.”, oddFibonacciNumbers, evenFibonacciNumbers ); 16.2. Управление циклом Что плохого может случиться с циклом? Любой ответ должен включать некоррек- тную или пропущенную инициализацию цикла, невыполненную инициализацию накопительных переменных (или других переменных, относящихся к циклу), неправильную вложенность, неправильное завершение цикла, отсутствие инкре- ментации переменной цикла или ее неправильную инкрементацию, а также неправильное индексирование элемента массива с помощью индекса цикла. Вы можете предотвратить эти проблемы, соблюдая два правила. Во-пер- вых, минимизируйте число факторов, влияющих на цикл. Упрощайте, упрощайте и еще раз упрощайте! Во-вторых, рассматривайте содержи- мое цикла так, будто это метод: вынесите за пределы цикла все управление, какое только возможно. Явно объявите все условия выполнения тела цикла. Не застав- ляйте читателя смотреть внутрь цикла, чтобы понять его управление. Думайте о цикле, как о черном ящике: окружающий код знает об управляющих условиях, но не о содержимом цикла. Пример представления цикла в виде черного ящика (C++) while ( !inputFile.EndOfFile() && moreDataAvailable ) { При каких условиях этот цикл завершится? Очевидно, все, что вам известно, что или inputFile.EndOfFile() станет исти- ной, или MoreDataAvailable станет ложью. Вход в цикл Следуйте принципам, приведенным далее, при разработке входа в цикл. Перекрестная ссылка Если вы используете технологию while ( true )-break, описанную выше, то условие выхода находится внутри черного ящика. Даже если вы используете только одно условие выхода, вы теря- ете преимущество рассмотрения цикла в виде черного ящика. 366 ЧАСТЬ IV Операторы Размещайте вход в цикл только в одном месте Разнообразие структур, управляющих циклом, позволяет проводить проверку его завершения в начале, се- редине или конце цикла. Эти структуры имеют достаточно широкие возможнос- ти, чтобы вы могли закодировать вход в цикл только сверху. Нет нужды делать вход в цикл в нескольких местах. Размещайте инициализационный код непосредственно перед циклом Принцип схожести пропагандирует размещение взаимосвязанных выражений вме- сте. Если взаимосвязанные выражения разбросаны по всем методу, то при внесе- нии исправлений их легко пропустить, сделав изменения не полностью. Если же взаимосвязанные выражения располагаются рядом, избежать ошибок при моди- фикации становится легче. Поместите операторы инициализации цикла рядом с этим циклом. Если вы этого не сделаете, то, вполне вероятно, это приведет к ошибкам, когда вы соберетесь преобразовать данный цикл в цикл большего размера и забудете исправить инициализационный код. Такая же ошибка может возник- нуть, когда вы переместите или скопируете код цикла в другой метод, забыв переместить инициализационный код. Размещение кода инициализации вдали от цикла — в разделе объявления данных или во вспомогательном разделе в начале метода — грозит неприятностями с ини- циализацией. Используйте while (true) для бесконечных циклов Вам может понадобить- ся цикл, выполняющийся без завершения, — например, цикл в таких изделиях, как кардиостимулятор или микроволновая печь. Или цикл должен завершаться толь- ко в ответ на событие — так называемый «событийный цикл». Вы можете закоди- ровать такой бесконечный цикл несколькими способами. Имитация цикла с по- мощью выражений вида for i = 1 to 99999 — плохая идея, поскольку конкретное значение границ цикла скрывает его смысл: 99999 может быть вполне допусти- мым значением. Кроме того, такой фальшивый бесконечный цикл плохо подда- ется сопровождению. Идиома while( true ) считается стандартным способом написания бесконечных циклов в C++, Java, Visual Basic и других языках, поддерживающих операции срав- нения. Некоторые программисты предпочитают использовать for( ;; ) — это при- емлемая альтернатива. Предпочитайте циклы for, если они применимы В цикле for управляющий код находится в одном месте, что способствует созданию легко читаемых циклов. При модификации ПО программисты часто делают ошибку, изменяя код иници- ализации в начале цикла while и забывая исправить соответствующий код в кон- це цикла. В цикле for необходимый код расположен в начале цикла, что упроща- ет модификацию кода. Если вы можете использовать цикл for вместо других цик- лов, сделайте это. Не используйте цикл for, если цикл while подходит больше Обычным зло- употреблением гибкой структурой цикла for в языках C++, C# и Java является раз- мещение частей цикла while в заголовке цикла for. Взгляните на цикл while, втис- нутый в заголовок цикла for: Перекрестная ссылка Об ограни- чении области видимости пере- менных цикла см. подраздел «Ограничьте видимость перемен- ных-индексов цикла самим цик- лом» далее в этой главе. ГЛАВА 16 Циклы 367 Пример цикла while, злостно втиснутого в заголовок цикла for (C++) // Чтение всех записей из файла. for ( inputFile.MoveToStart(), recordCount = 0; !inputFile.EndOfFile(); recordCount++ ) { inputFile.GetRecord(); } Преимущество цикла for в языке C++ по сравнению с другими языками состоит в его большей гибкости по отношению к информации, которую он может исполь- зовать для инициализации и завершения. Недостатком такой гибкости является возможность помещения в заголовок цикла выражений, не имеющих ничего об- щего с управлением циклом. Зарезервируйте заголовок цикла for для выражений, управляющих циклом: выпол- няющих инициализацию, завершение и движение к завершению. В приведенном примере выражение inputFile.GetRecord() в теле цикла продвигает цикл в сторону завершения, а выражения recordCount — нет; это вспомогательные выражения, не управляющие циклом. Размещение recordCount в заголовке цикла, а inputFile.Get- Record() — вне его создает путаницу и фальшивое впечатление, что recordCount управляет циклом. Если в данном случае вы хотите использовать цикл for, а не while, поместите управляющие выражения в заголовок, а все остальные из него уберите. Вот пра- вильный способ использования заголовка цикла: Пример логичного, хоть и нетрадиционного использования заголовка цикла for (C++) recordCount = 0; for ( inputFile.MoveToStart(); !inputFile.EndOfFile(); inputFile.GetRecord() ) { recordCount++; } Все содержимое заголовка цикла в этом примере относится к управлению цик- лом. Выражение inputFile.MoveToStart() инициализирует цикл, выражение !inputFile- .EndOfFile() проверяет его завершение, а inputFile.GetRecord() продвигает цикл в сторону завершения. Выражения, относящиеся к recordCount, не продвигают цикл в сторону завершения напрямую и поэтому вполне уместно не включены в заго- ловок цикла. Возможно, цикл while все же больше подходит для этой работы, но этот код по крайней мере логично использует заголовок цикла. Для галочки по- кажем, как будет выглядеть этот код при использовании цикла while: Пример соответствующего использования цикла while (C++) // Чтение всех записей из файла. inputFile.MoveToStart(); recordCount = 0; while ( !inputFile.EndOfFile() ) { inputFile.GetRecord(); recordCount++; } 368 ЧАСТЬ IV Операторы Обработка середины цикла Следующие подразделы описывают обработку середины цикла: Используйте { и } для обрамления выражений в цикле Всегда используйте скобки. Они ничего не стоят в плане скорости или размера во время выполне- ния, но повышают читабельность и предотвращают ошибки при изменении кода. Это хорошая практика защитного программирования. Избегайте пустых циклов В C++ и Java возможно создание пустого цикла, все действия которого закодированы в той же строке, что и проверка выхода из цик- ла. Вот пример: Пример пустого цикла (C++) while ( ( inputChar = dataFile.GetChar() ) != CharType_Eof ) { ; } Здесь тело цикла пустое, потому что само выражение while делает две вещи: вы- полняет циклические действия ( inputChar = dataFile.GetChar()) и проверяет, завер- шить ли работу цикла ( inputChar != CharType_Eof). Цикл будет яснее, если его пе- рекодировать так, чтобы его работа была более очевидной читателю: Пример пустого цикла, преобразованного в полноценный цикл (C++) do { inputChar = dataFile.GetChar(); } while ( inputChar != CharType_Eof ); Новый код занимает три полных строки по сравнению с одной строкой и точ- кой с запятой. Но это допустимо, так как он и выполняет работу для трех строк, а не для одной. Располагайте служебные операции либо в начале, либо в конце цикла Служебные операции цикла — это выражения вроде i = i + 1 или j++, чье основ- ное назначение не выполнять работу в цикле, а управлять циклом. В этом приме- ре показаны служебные действия, выполняемые в конце цикла: Пример служебных выражений, расположенных в конце цикла (C++) nameCount = 0; totalLength = 0; while ( !inputFile.EndOfFile() ) { // Выполняем работу цикла inputFile >> inputString; names[ nameCount ] = inputString; // Готовимся к следующей итерации цикла — служебные действия. Вот служебные операторы. nameCount++; totalLength = totalLength + inputString.length(); } > ГЛАВА 16 Циклы 369 Как правило, переменные, которые вы инициализируете перед циклом, и есть те переменные, которыми вы манипулируете в служебной части цикла. Заставьте каждый цикл выполнять только одну функцию Простой факт, что цикл может использоваться для выполнения двух дел одновременно, — недостаточное оправдание для их совмещения. Циклы должны быть подобны методам в том плане, что каждый должен делать только одно дело и делать его хорошо. Если использо- вание двух циклов, когда хватит и одного, кажется неэффективным, напишите код в виде двух циклов, прокомментируйте, что их можно объединить для эффектив- ности, и дождитесь, пока тесты оценки производительности покажут проблему в этом месте. Только после этого объединяйте два цикла в один. Завершение цикла Следующие подразделы описывают обработку конца цикла. Убедитесь, что выполнение цикла закончилось Это основной принцип. Мыс- ленно моделируйте выполнение цикла до тех пор, пока не будете уверены, что при любых обстоятельствах он завершен. Продумайте номинальные варианты, граничные точки и каждый из исключительных случаев. Сделайте условие завершения цикла очевидным Если вы используете цикл for, не забавляетесь с индексом цикла и не применяете операторы goto или break для выхода из него, то условие завершения будет очевидным. Аналогично, если вы используете циклы while или repeat-until и поместили все управление в выра- жение while или repeat-until, условие завершения также будет очевидным. Смысл в том, чтобы размещать управление в одном месте. Не играйте с индексом цикла for для завершения цикла Некоторые про- граммисты взламывают значение индекса цикла for для более раннего заверше- ния цикла. Вот пример: Пример неправильного обращения с индексом цикла (Java) for ( int i = 0; i < 100; i++ ) { // Некоторый код if ( ... ) { Здесь индекс портится. i = 100; } // Еще код } Смысл этого примера в завершении цикла при каком-то условии с помощью установки значения i в 100, что больше, чем границы диапазона цикла for от 0 до 99. Фактически все хорошие программисты избегают такого способа — это при- Перекрестная ссылка Об опти- мизации см. главы 25 и 26. > 370 ЧАСТЬ IV Операторы знак любительского подхода. Когда вы задаете цикл for, манипуляции с его счет- чиком должны быть под запретом. Для получения большей управляемости усло- виями выхода используйте цикл while. Избегайте писать код, зависящий от последнего значения индекса цикла Использование значения индекса цикла после его завершения — дурной тон. Конечное значение индекса меняется от языка к языку и от реализации к реали- зации. Значения различаются, когда цикл завершается нормально или аномаль- но. Даже если вы не задумываясь можете назвать это конечное значение, следую- щему читателю кода, возможно, придется о нем задуматься. Более правильным вариантом, к тому же более самодокументируемым, будет присвоение последне- го значения какой-либо переменной в подходящем месте внутри цикла. Этот код некорректно использует конечное значение индекса: Пример кода, который неправильно применяет последнее значение индекса цикла (C++) for ( recordCount = 0; recordCount < MAX_RECORDS; recordCount++ ) { if ( entry[ recordCount ] == testValue ) { break; } } // Много кода Здесь неправильное применение завершающего значения индекса цикла. if ( recordCount < MAX_RECORDS ) { return( true ); } else { return( false ); } В этом фрагменте вторая проверка recordCount < MaxRecords производит впечат- ление, что цикл будет проходить по всем элементам entry[] и вернет true, если найдет значение, равное testValue, и false в противном случае. Тяжело помнить, будет ли индекс инкрементироваться после конца цикла, поэтому легко сделать ошибку потери единицы. Лучше переписать код так, чтобы он не зависел от последнего значения индекса. Вот пример обновленного кода: Пример кода, который не делает ошибки при использовании последнего значения индекса цикла (C++) found = false; for ( recordCount = 0; recordCount < MAX_RECORDS; recordCount++ ) { if ( entry[ recordCount ] == testValue ) { found = true; break; } } > ГЛАВА 16 Циклы 371 // Много кода return( found ); Этот второй фрагмент использует дополнительную переменную и располагает обращения к recordCount в более ограниченном пространстве. Как часто бывает при применении вспомогательной логической переменной, результирующий код становится яснее. Рассмотрите использование счетчиков безопасности Счетчик безопасно- сти — это переменная, увеличивающаяся при каждом проходе цикла, чтобы определить, не слишком ли много раз выполняется цикл. Если вы пишете програм- му, в которой любая ошибка будет катастрофической, вы можете использовать счетчики безопасности, чтобы убедиться, что все циклы заканчиваются. Такой цикл на C++ вполне может использовать счетчик безопасности: Пример цикла, который мог бы использовать счетчик безопасности (C++) do { node = node->Next; } while ( node->Next != NULL ); Вот тот же код с добавленным счетчиком безопасности: Пример использования счетчика безопасности (C++) safetyCounter = 0; do { node = node->Next; Здесь код счетчика безопасности. safetyCounter++; if ( safetyCounter >= SAFETY_LIMIT ) { Assert( false, “Internal Error: Safety-Counter Violation.” ); } } while ( node->Next != NULL ); Счетчики безопасности не панацея. Добавляемые в код по одному, они увеличи- вают сложность и могут привести к дополнительным ошибкам. Так как они не при- меняются в каждом цикле, вы можете забыть поддержать код счетчика при моди- фикации циклов в той части программы, где они все же используются. Но если счетчики безопасности вводятся на уровне проектного стандарта для критичес- ких циклов, вы будете ожидать их, и код этих счетчиков будет не более подвер- жен ошибкам, чем любой другой. Досрочное завершение цикла Многие языки предоставляют средства для завершения цикла без выполнения условий for или while. В данном обсуждении слово break обозначает общий тер- > 372 ЧАСТЬ IV Операторы мин для оператора break в C++, C и Java; выражений Exit-Do и Exit-For в Visual Basic и подобных конструкций, включая имитации с помощью goto, в языках, не под- держивающих break напрямую. Оператор break (или его эквивалент) приводит к завершению цикла через нормальный канал выхода. Программа продолжает вы- полнение с первого оператора, расположенного после цикла. Оператор continue похож на break в том смысле, что это вспомогательное сред- ство для управления циклом. Однако вместо выхода из цикла, continue заставляет программу пропустить тело цикла и продолжить выполнение со следующей ите- рации. Оператор continue — это сокращенный вариант блока if-then, предотвра- щающего выполнение остальной части цикла. Рассмотрите использование операторов break вместо логических фла- гов в цикле while Порой добавление логических флагов в цикл while с целью имитации выхода из тела цикла усложняет чтение кода. Иногда вы можете убрать несколько уровней отступа в цикле и упростить его управление, просто исполь- зуя break вместо группы проверок if. Размещение нескольких отдельных условий break рядом с кодом, приводящим к их выполнению, может уменьшить вложен- ность и сделать цикл читабельнее. Остерегайтесь цикла с множеством операторов break, разбросанных по всему коду Цикл, содержащий большое количество операторов break, может сиг- нализировать о нечетком представлении структуры цикла или его роли в окру- жающем коде. Рост числа break увеличивает вероятность, что цикл может быть более ясно представлен в виде набора нескольких циклов вместо одного цикла с мно- жеством выходов. Согласно статье в «Software Engineering Notes» программная ошибка, которая 15 января 1990 года на 9 часов вывела из строя телефонную сеть Нью-Йорка, воз- никла благодаря лишнему оператору break.(SEN, 1990): Пример ошибочного использования оператора break в блоке do-switch-if (C++) do { switch if () { Этот break предназначался для if, но вместо этого привел к выходу из switch. break; } } while ( ... ); Большое количество break не обязательно означает ошибку, но их присутствие в цикле — тревожный сигнал: как канарейка в шахте, задыхающаяся из-за недостатка воздуха, вместо того чтобы петь. > |