Файл: Руководство по стилю программирования и конструированию по.pdf
ВУЗ: Не указан
Категория: Не указан
Дисциплина: Не указана
Добавлен: 30.11.2023
Просмотров: 839
Скачиваний: 2
ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.
СОДЕРЖАНИЕ
ГЛАВА 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 не обязательно означает ошибку, но их присутствие в цикле — тревожный сигнал: как канарейка в шахте, задыхающаяся из-за недостатка воздуха, вместо того чтобы петь.
>
1 ... 42 43 44 45 46 47 48 49 ... 104
ГЛАВА 16 Циклы
373
Используйте continue для проверок в начале цикла Хорошим применени- ем оператора
continue будет перемещение операций в конец тела цикла после про- верки некоторого условия в его начале. Например, если цикл читает записи, от- брасывает часть из них, а остальные обрабатывает, вы можете поместить подоб- ную проверку в начало цикла:
Пример относительно безопасного использования continue (псевдокод)
while ( not eof( file ) ) do read( record, file )
if ( record.Type <> targetType ) then continue
— Обрабатываем запись targetType.
end while
Такое использование
continue позволяет избегать проверок if, что эффективно уменьшит отступы внутри всего тела цикла. С другой стороны, если
continue воз- никает в середине или конце цикла, используйте вместо него
if.
Используйте структуру break с метками, если ваш язык ее поддержи-
вает Java поддерживает помеченные операторы break, что позволяет предотв- ратить проблемы, приведшие к выходу из строя телефонов в Нью-Йорке.
break с меткой можно использовать для выхода из цикла
for, условия if или любого блока кода, заключенного в скобки (Arnold, Gosling and Holmes, 2000).
Вот возможное решение «нью-йоркской проблемы», переписанное на Java вмес- то C++, что позволяет использовать
break с меткой:
Пример лучшего использования помеченного оператора break
в блоке do-switch-if (Java)
do {
switch
CALL_CENTER_DOWN:
if () {
Назначение помеченного break однозначно.
break CALL_CENTER_DOWN;
}
} while ( ... );
Используйте операторы break и continue очень осторожно Применение
break исключает возможность представления цикла в виде черного ящика. Если вы ограничиваетесь только одним выражением для управления условием выхода из цикла, то получаете мощное средство для упрощения циклов. Применение
break
>
374
ЧАСТЬ IV Операторы заставляет читателя смотреть внутрь цикла, чтобы разобраться в его управлении.
Это усложняет понимание цикла.
Используйте
break только после того, как рассмотрели все альтернативы. Вы не можете сказать с уверенностью, хороши или плохи конструкции
continue и break.
Некоторые ученые утверждают, что это допустимые технологии в структурном программировании, а некоторые — что нет. Поскольку вы не знаете, правильно ли применять
continue и break вообще, используйте их, но не забывайте, что вы можете быть неправы. На самом деле это сводится к простому утверждению: если вы не можете аргументировать применение
break или continue, не применяя их.
Проверка граничных точек
При разработке цикла обычно представляют интерес три точки: первая итерация,
случайно выбранная итерация в середине и последняя итерация. Когда вы созда- ете цикл, мысленно пройдитесь по этим трем точкам и убедитесь, что в цикле нет ошибки потери единицы. Если цикл содержит какие-то специальные случаи, вы- полнение которых отличается от первой или последней итерации, проверьте их тоже. Если цикл производит сложные вычисления, достаньте свой калькулятор и проверьте их вручную.
Готовность выполнять такой вид проверки — ключевое различие между квалифицированными и неквалифицированными программистами. Пер- вые проделывают мысленное моделирование и вычисления вручную,
потому что знают, что эти меры помогут им найти ошибки.
Вторые имеют склонность к случайному экспериментированию, пока не найдут правдоподобную комбинацию. Если цикл не работает так, как предполагалось,
неумелый программист меняет знак
< на <=. Если и это не помогает, он исправ- ляет индекс цикла, добавляя или вычитая 1. В конечном счете таким способом программист может нащупать правильную комбинацию или просто заменить изначальную ошибку более незаметной. Даже если этот случайный процесс при- ведет к правильной программе, программист не будет знать, почему она работа- ет корректно.
Мысленное моделирование и ручные вычисления могут дать несколько преиму- ществ. Умственная тренировка приводит к меньшему количеству ошибок при первоначальном кодировании, более быстрому обнаружению проблем при отладке и в целом более полному пониманию программы. Умственные упражнения озна- чают, что вы знаете, как работает код, а не просто предполагаете это.
Использование переменных цикла
Далее описаны некоторые принципы применения переменных цикла.
Используйте порядковые или перечислимые типы для
границ массивов и циклов Обычно счетчики циклов должны быть целыми значениями. Числа с плавающей за- пятой плохо инкрементируются. Например, вы можете при- бавить 1,0 к 26 742 897,0 и получить 26 742 897,0 вместо
26 742 898,0. Если это число используется как индекс цикла, вы получите беско- нечный цикл.
Перекрестная ссылка Об имено- вании переменных цикла см.
подраздел «Именование индек- сов циклов» раздела 11.2.
ГЛАВА 16 Циклы
375
Используйте смысловые имена переменных, чтобы сделать вло-
женные циклы читабельными Массивы часто индексируются с по- мощью тех же переменных, что используются как индексы цикла. Если у вас одномерный массив, то вы еще сможете выйти сухим их воды, применяя
i, j
или
k для его индексации. Но если у массива два и более измерений, вам следует задавать значимые имена для индексов, чтобы прояснить свои действия. Смысло- вые имена индексов массивов одновременно уточняют и назначение цикла, и эле- мент массива, к которому вы планируете обратиться.
Вот пример кода, который не применяет этот принцип: в нем использованы бес- смысленные имена
i, j и k:
Пример неправильных имен
переменных цикла (Java)
for ( int i = 0; i < numPayCodes; i++ ) {
for ( int j = 0; j < 12; j++ ) {
for ( int k = 0; k < numDivisions; k++ ) {
sum = sum + transaction[ j ][ i ][ k ];
}
}
}
Как вы думаете, что означают индексы в элементе
transaction? Сообщают ли пе- ременные
i, j и k что-либо о содержимом transaction? Если вы знаете объявление
transaction, можете ли вы легко определить, указаны ли индексы в правильном порядке? Вот тот же цикл с более читабельными именами переменных:
Пример хороших имен переменных цикла на Java
for ( int payCodeIdx = 0; payCodeIdx < numPayCodes; payCodeIdx++ ) {
for (int month = 0; month < 12; month++ ) {
for ( int divisionIdx = 0; divisionIdx < numDivisions; divisionIdx++ ) {
sum = sum + transaction[ month ][ payCodeIdx ][ divisionIdx ];
}
}
}
Как вы думаете, что означают индексы в элементе
transaction на этот раз? В этом случае ответ получить проще, потому что имена переменных
payCodeIdx, month
и
divisionIdx гораздо красноречивее, чем i, j и k. Компьютер с одинаковой легко- стью прочитает обе версии цикла. Однако людям легче будет читать вторую вер- сию, чем первую, поэтому второй вариант лучше, поскольку ваша основная ауди- тория состоит из людей, а не из компьютеров.
Используйте смысловые имена во избежание пересечения индексов При- вычное использование переменных
i, j и k приводит к увеличению риска пересе- чения индексов — использованию одного и того же имени индекса для разных целей. Взгляните:
376
ЧАСТЬ IV Операторы
Пример пересечения индексов (C++)
i сначала используется здесь...
for ( i = 0; i < numPayCodes; i++ ) {
// много кода for ( j = 0; j < 12; j++ ) {
// много кода
...а теперь здесь for ( i = 0; i < numDivisions; i++ ) {
sum = sum + transaction[ j ][ i ][ k ];
}
}
}
Применение
i настолько привычно, что эта переменная используется в одной вложенной структуре дважды. Второй цикл
for, управляемый i, конфликтует с пер- вым — это и есть пересечение индексов. Применение более значимых имен, чем
i, j и k, предотвратило бы проблему. Вообще, если тело цикла содержит больше пары строк кода, или может вырасти, или входит в группу вложенных циклов, из- бегайте переменных
i, j и k.
Ограничивайте видимость переменных-индексов цикла самим циклом Пе- ресечение индексов цикла и другое применение индексов вне самих циклов —
настолько важная проблема, что разработчики языка Ada решили сделать индек- сы цикла
for недоступными вне цикла. Попытка использования переменной-ин- декса вне цикла
for приводит к ошибке времени компиляции.
C++ и Java в какой-то мере реализуют ту же идею — они позволяют объявлять индексы цикла в нем самом, но не требуют этого. Выше, в примере раздела «Из- бегайте писать код, зависящий от последнего значения индекса цикла», перемен- ная
recordCount может быть объявлена внутри выражения for, что ограничит ее область видимости этим циклом:
Пример объявления переменной-индекса цикла внутри цикла for (C++)
for ( int recordCount = 0; recordCount < MAX_RECORDS; recordCount++ ) {
// Циклический код, использующий recordCount.
}
В принципе эта методика должна позволять создавать код, повторно объявляю- щий переменную
recordCount в нескольких циклах без риска неправильного ис- пользования двух разных
recordCount. Такое применение позволило бы писать,
например, такой код:
Пример объявления переменных-индексов внутри циклов for
и их (возможно!) безопасное повторное использование (C++)
for ( int recordCount = 0; recordCount < MAX_RECORDS; recordCount++ ) {
// Циклический код, использующий recordCount.
}
>
>