Совершенный код. Совершенный код. Мастер-класс. Стив Макконнелл. Руководство по стилю программирования и конструированию по
Скачать 5.88 Mb.
|
ГЛАВА 16 Циклы 363 Разместите все условия выхода в одном месте. Распростра# нение их по коду практически гарантирует, что то или иное условие завершения будет пропущено при отладке, модифи# кации или тестировании. Пишите комментарии для ясности. Если вы применяете цикл с выходом в языке, который не поддерживает его напрямую, используйте комментарии, чтобы сделать свои действия очевидными. Цикл с выходом — структурированная управляющая конструкция, име# ющая один вход и один выход. Такая структура является предпочтитель# ным вариантом цикла (Software Productivity Consortium, 1989). Доказано, что этот тип цикла легче для понимания, чем другие. Группа студентов#програм# мистов сравнила такой цикл с другими вариантами, имеющими выход в начале или конце (Soloway, Bonar и Ehrlich, 1983). Тесты на понимание для цикла с выхо# дом выполнялись студентами на 25% успешнее. Авторы курса пришли к выводу, что структура цикла с выходом лучше, чем другие циклы, моделирует способ че# ловеческого представления итеративного процесса. В повседневной практике цикл с выходом пока еще не широко распространен. Присяжные все еще заперты в накуренной комнате, споря о том, годиться ли эта методика для промышленного кода. Пока они там томятся, цикл с выходом будет хорошим инструментом в вашем программистском наборе — при условии его ак# куратного использования. Аномальные циклы с выходом Другой вид цикла с выходом служит для замены следующего варианта «полутор# ного» цикла: Пример входа в середину цикла с помощью goto — плохая практика (C++) goto Start; while ( expression ) { // Делаем чтото. Start: // Делаем чтото еще. } На первый взгляд, этот цикл похож на предыдущие примеры цикла с выходом. Он используется, если выражение, обозначенное как // делаем что%то, не должно выполняться при первом проходе цикла, а выражение // делаем что%то еще — должно. Это тоже конструкция с одним входом и выходом: единственный вход в цикл — через оператор goto в начале, а выход — с помощью условия while. Этот подход содержит две проблемы: он использует goto и довольно необычен, чем сби# вает с толку. Перекрестная ссылка Другие сведения об условиях заверше- ния представлены ниже в этой главе. Об использовании ком- ментариев в циклах см. подраз- дел «Комментирование управля- ющих структур» раздела 32.5. 364 ЧАСТЬ IV Операторы В C++ вы можете добиться того же эффекта без использования goto, как показано в следующем примере. Если язык не поддерживает команду break, вы можете эму# лировать ее, применив goto. Пример кода, переписанного без использования goto — лучший вариант (C++) while ( true ) { Блоки перед и после break поменяны местами. // Делаем чтото еще. if ( !( expression ) ) { break; } // Делаем чтото. } Когда использовать цикл for Цикл for — хороший вариант, если вам нужен цикл, выпол# няющийся определенное количество раз. Вы можете исполь# зовать for в C++, C, Java, Visual Basic и большинстве других языков. Применяйте циклы for в простых случаях, не требующих управления изнутри тела цикла. Используйте их, когда управление циклом заклю# чается в простом инкременте или декременте, скажем, при проходе по элемен# там контейнера. Особенность цикла for в том, что его надо настроить в начале выполнения и забыть о нем. Вам ничего не надо делать внутри него для управле# ния его работой. Если существует условие, по которому выполнение цикла пре# рывается изнутри, вместо for используйте конструкцию while. Не изменяйте значение индекса цикла for явно, чтобы принудительно его завершить. Вместо этого используйте while. Цикл for предназначен для простых случаев. Более сложные задачи организации циклов лучше решать с помощью цикла while. Когда использовать цикл foreach Цикл foreach (или его эквиваленты For%Each в Visual Basic, for%in в Python) полезен для выполнения действий над каждым элементом массива или другого контейне# ра. Его преимущество в том, что он позволяет обойтись без вспомогательной ариф# метики для обслуживания цикла и, таким образом, избежать ошибок. Вот пример такого цикла: Дополнительные сведения О других хороших приемах исполь- зования циклов for см. в «Writ- ing Solid Code» (Maguire, 1993). ГЛАВА 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; } } > |