Файл: Руководство по стилю программирования и конструированию по.pdf
ВУЗ: Не указан
Категория: Не указан
Дисциплина: Не указана
Добавлен: 30.11.2023
Просмотров: 843
Скачиваний: 2
ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.
СОДЕРЖАНИЕ
ГЛАВА 16 Циклы
361
Цикл с проверкой в начале
В качестве цикла с проверкой в начале вы можете использовать цикл
while в язы- ках C++, C#, Java, Visual Basic и многих других. Вы также можете эмулировать цикл
while в других языках.
Цикл с проверкой в конце
Время от времени вам требуется гибкий цикл, который должен выполняться хотя бы раз. В этом случае можно применить
while с проверкой условия в конце цик- ла. Вы можете использовать варианты
do-while в C++, C# и Java, Do-Loop-While вVisual
Basic или эмулировать циклы с проверкой в конце в других языках
Когда использовать цикл с выходом
В цикле с выходом условие выхода содержится внутри тела цикла, а не в его на- чале или конце. Циклы с выходом реализованы исключительно в Visual Basic, но вы можете эмулировать их, используя структурированные конструкции
while и break
в C++, C и Java или операторы
goto в других языках.
Нормальные циклы с выходом
Цикл с выходом обычно состоит из начала цикла, тела цикла (включая условие выхода) и конца цикла:
Пример обычного цикла с выходом (Visual Basic)
Do
Операторы.
If ( some exit condition ) Then Exit Do
Еще операторы.
Loop
Обычно цикл с выходом нужен, когда проверка условия в начале или конце цик- ла требует кодирования полутора циклов. Вот пример на C++, в котором нужен цикл с выходом, но он не используется:
Пример дублированного кода, который подвержен ошибкам при сопровождении (C++)
// Вычисляем счет и рейтинги.
score = 0;
Эти строки появляются здесь...
GetNextRating( &ratingIncrement );
rating = rating + ratingIncrement;
while ( ( score < targetScore ) && ( ratingIncrement != 0 ) ) {
GetNextScore( &scoreIncrement );
score = score + scoreIncrement;
>
>
>
362
ЧАСТЬ IV Операторы
…и повторяются здесь.
GetNextRating( &ratingIncrement );
rating = rating + ratingIncrement;
}
Первые две строки в начале примера повторяются в последних двух строках цикла
while. При модификации вы легко можете забыть о поддержании двух параллель- ных наборов строк. Другой программист, изменяющий код, возможно, и не дога- дается, что эти строки должны сохраняться одинаковыми. В любом случае резуль- татом будут ошибки, возникшие из-за неполной модификации. Вот как можно переписать код более ясно:
Пример цикла с выходом, более легкого в сопровождении (C++)
// Вычисляем счет и рейтинги. Этот код использует бесконечный цикл
// и оператор break для имитации цикла с выходом.
score = 0;
while ( true ) {
GetNextRating( &ratingIncrement );
rating = rating + ratingIncrement;
Это условие выхода из цикла (и теперь оно может быть упрощено с помощью теорем ДеМоргана,
описанных в разделе 19.1).
if ( !( ( score < targetScore ) && ( ratingIncrement != 0 ) ) ) {
break;
}
GetNextScore( &scoreIncrement );
score = score + scoreIncrement;
}
Вот как тот же код можно написать на Visual Basic:
Пример цикла с выходом (Visual Basic)
‘ Вычисляем счет и рейтинги.
score = 0
Do
GetNextRating( ratingIncrement )
rating = rating + ratingIncrement
If ( not ( score < targetScore and ratingIncrement <> 0 ) ) Then Exit Do
GetNextScore( ScoreIncrement )
score = score + scoreIncrement
Loop
При использовании этого типа циклов учитывайте следующие тонкие моменты.
>
>
ГЛАВА 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).
1 ... 41 42 43 44 45 46 47 48 ... 104
ГЛАВА 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();
}
>