Руководство по стилю программирования и конструированию по
Скачать 7.6 Mb.
|
ГЛАВА 14 Организация последовательного кода 345 Ключевые моменты 쐽 Главный принцип организации последовательного кода — упорядочение за- висимостей. 쐽 Зависимости должны быть сделаны явными с помощью хороших имен мето- дов, списков параметров, комментариев и — если последовательность кода достаточно критична — с помощью вспомогательных переменных. 쐽 Если порядковые зависимости в коде отсутствуют, старайтесь размещать вза- имосвязанные выражения как можно ближе друг к другу. 346 ЧАСТЬ IV Операторы Г Л А В А 1 5 Условные операторы Содержание 쐽 15.1. Операторы if 쐽 15.2. Операторы case Связанные темы 쐽 Укрощение глубокой вложенности: раздел 19.4 쐽 Общие вопросы управления: глава 19 쐽 Код с операторами цикла: глава 16 쐽 Последовательный код: глава 14 쐽 Отношения между типами данных и управляющими структурами: раздел 10.7 Условный оператор управляет выполнением других операторов. Их выполнение «обусловливают» такие операторы, как if, else, case и switch. Хотя операторы цик- ла, например while и for, по смыслу тоже можно отнести к условным, обычно их рассматривают отдельно. Операторы while и for мы обсудим в главе 16. 15.1. Операторы if В зависимости от выбранного языка программирования вы можете использовать несколько видов if-операторов. Простейшие из них — if или if-then. Оператор if- then-else немного сложнее, а наибольшую сложность представляют последователь- ности if-then-else-if. Простые операторы if-then Следуйте этим правилам при написании if-выражений. Сначала напишите код номинального хода алгоритма, затем опишите исключительные случаи Пишите код так, чтобы нормаль- ный путь выполнения был очевиден. Убедитесь, что нестандартные об- стоятельства не затмевают смысл основного алгоритма. Это важно как с точки зрения читабельности, так и с точки зрения производительности. Убедитесь, что при сравнении на равенство ветвление корректно Исполь- зование > вместо >= или < вместо <= — это аналог ошибки потери единицы при http://cc2e.com/1538 ГЛАВА 15 Условные операторы 347 обращении к массиву или вычислении индекса цикла. Чтобы ее избежать, в опе- раторе цикла следует рассматривать граничные точки, а в условных операторах — учитывать случаи равенства. Размещайте нормальный вариант после if, а не пос- ле else Пишите код так, чтобы нормальный вариант разви- тия событий обрабатывался в первую очередь. Это совпада- ет с главным принципом размещения действий, являющих- ся результатом выбора, как можно ближе к точке этого вы- бора. Вот пример кода, который выполняет неоднократную проверку ошибок, бес- порядочно разбросанную по тексту: Пример кода, который беспорядочно обрабатывает многочисленные ошибки (Visual Basic) OpenFile( inputFile, status ) If ( status = Status_Error ) Then Ошибочная ситуация. errorType = FileOpenError Else Нормальная ситуация. ReadFile( inputFile, fileData, status ) If ( status = Status_Success ) Then Нормальная ситуация. SummarizeFileData( fileData, summaryData, status ) If ( status = Status_Error ) Then Ошибочная ситуация. errorType = ErrorType_DataSummaryError Else Нормальная ситуация. PrintSummary( summaryData ) SaveSummaryData( summaryData, status ) If ( status = Status_Error ) Then Ошибочная ситуация. errorType = ErrorType_SummarySaveError Else Нормальная ситуация. UpdateAllAccounts() EraseUndoFile() errorType = ErrorType_None End If End If Else Перекрестная ссылка Другие способы обращения с кодом обработки ошибок описаны в разделе 19.4. > > > > > > > 348 ЧАСТЬ IV Операторы errorType = ErrorType_FileReadError End If End If Этот код сложен для понимания, так как в нем перемешаны нормальные и оши- бочные ситуации. Тяжело проследить путь, проходимый в коде при нормальных обстоятельствах. Кроме того, так как ошибки иногда обрабатываются в блоке if, а не else, тяжело найти, в каких же ветвях if обрабатываются нормальные ситуации. В переписанном примере нормальный путь последовательно кодируется первым, а ошибочные ситуации — последними. Это упрощает поиск и чтение номиналь- ного варианта алгоритма. Пример кода, который систематично обрабатывает большое количество ошибок (Visual Basic) OpenFile( inputFile, status ) If ( status = Status_Success ) Then Нормальная ситуация. ReadFile( inputFile, fileData, status ) If ( status = Status_Success ) Then Нормальная ситуация. SummarizeFileData( fileData, summaryData, status ) If ( status = Status_Success ) Then Нормальная ситуация. PrintSummary( summaryData ) SaveSummaryData( summaryData, status ) If ( status = Status_Success ) Then Нормальная ситуация. UpdateAllAccounts() EraseUndoFile() errorType = ErrorType_None Else Ошибочная ситуация. errorType = ErrorType_SummarySaveError End If Else Ошибочная ситуация. errorType = ErrorType_DataSummaryError End If Else Ошибочная ситуация. errorType = ErrorType_FileReadError End If Else > > > > > > > ГЛАВА 15 Условные операторы 349 Ошибочная ситуация. errorType = ErrorType_FileOpenError End If Здесь можно проследить главное направление if-проверок, чтобы выяснить нор- мальный вариант событий. Этот фрагмент позволяет фокусировать чтение в ос- новном направлении, а не преодолевать исключительные ситуации, поэтому этот код в целом читабельнее. Стек ошибочных условий, расположенный внизу вло- жения, — признак хорошо написанного кода обработки ошибок. Этот пример иллюстрирует один систематический подход к обработке нормаль- ных и ошибочных ситуаций. Другие решения этой проблемы: использование пре- дохранительных конструкций, диспетчеризация полиморфных объектов, выне- сение внутренних проверок в отдельные методы — обсуждаются на протяжении всей книги. Полный список существующих подходов см. в разделе 19.4. Размещайте осмысленные выражения после оператора if Иногда можно встретить код, в котором блок if пуст: Пример пустого блока if (Java) if ( SomeTest ) ; else { // делаем что-то } Опытные программисты не станут писать такой код хотя бы затем, чтобы избежать лишней работы по вводу пустой стро- ки и оператора else. Этот код выглядит глупо и может быть легко улучшен путем отрицания предиката в выражении if, перемещения кода из блока else в блок if и удаления блока else. Вот как будет выглядеть код после таких изменений: Пример преобразования пустого блока if (Java) if ( ! someTest ) { // делаем что-то } Рассмотрите вопрос использования блока else Если вы считаете, что вам нужен простой оператор if, подумайте, может, вам на самом деле нужен вариант с if-then-else. Классический анализ General Motors пока- зал, что в 50–80% случаев использования операторов if следовало применять и оператор else (Elshoff, 1976). Одна из причин добавления блока else — даже пустого — в том, чтобы продемон- стрировать, что вариант с else был учтен. Конечно, кодирование пустых выраже- ний в else просто для того, чтобы показать, что этот вариант рассмотрен, может Перекрестная ссылка Один из ключей к конструированию эф- фективного оператора if- — со- здание правильного управляюще- го логического выражения. Об эф- фективном применении логичес- ких выражений см. раздел 19.1. > 350 ЧАСТЬ IV Операторы быть преувеличением, но хотя бы принимайте вариант с else во внимание. Если вы задаете if-проверку, не имеющую блока else, то, кроме очевидных случаев, пи- шите в комментариях объяснение, почему else отсутствует, скажем, так: Пример полезного, прокомментированного блока elsе (Java) // Если цвет задан корректно. if ( COLOR_MIN <= color && color <= COLOR_MAX ) { // Делаем что-то } else { // Иначе цвет задан некорректно. // Вывод на экран не выполняется –- просто игнорируем команду. } Проверяйте корректность выражения else При тестировании кода вы мо- жете решить, что достаточно проверить основной блок if и все. Однако, если можно проверить вариант в else, не забудьте это сделать. Проверяйте возможную перестановку блоков if и else Частая ошибка при программировании выражений if-then состоит в размещении кода из блока if в блоке else, т. е. инвертировании логики выражения if. Проверяйте ваш код на наличие этой ошибки. Последовательности операторов if-then-else Если язык не поддерживает операторы case или поддерживает их только частич- но, вам часто придется писать последовательные проверки if-then-else. К приме- ру, код распределения символов по категориям, может выглядеть в виде такой цепочки: Пример использования последовательности if-then-else для распределения символов по категориям (C++) if ( inputCharacter < SPACE ) { characterType = CharacterType_ControlCharacter; } else if ( inputCharacter == ‘ ‘ || inputCharacter == ‘,’ || inputCharacter == ‘.’ || inputCharacter == ‘!’ || inputCharacter == ‘(‘ || inputCharacter == ‘)’ || inputCharacter == ‘:’ || inputCharacter == ‘;’ || inputCharacter == ‘?’ || inputCharacter == ‘-’ ) { characterType = CharacterType_Punctuation; } Перекрестная ссылка Об упро- щении сложных выражений см. раздел 19.1. ГЛАВА 15 Условные операторы 351 else if ( ‘0’ <= inputCharacter && inputCharacter <= ‘9’ ) { characterType = CharacterType_Digit; } else if ( ( ‘a’ <= inputCharacter && inputCharacter <= ‘z’ ) || ( ‘A’ <= inputCharacter && inputCharacter <= ‘Z’ ) ) { characterType = CharacterType_Letter; } Учитывайте советы, приведенные далее, при написании последовательных if-then-else. Упрощайте сложные проверки с помощью вызовов логических функций Одна из причин, по которой код из предыдущего примера сложно читать, в том, что проверки категорий символов довольно сложны. Для улучшения читабельно- сти вы можете заменить их вызовами функций, возвращающих логические зна- чения. Вот как этот пример может выглядеть после замены условий логическими функциями: Пример последовательности if-then-else, использующей вызовы логических функций (C++) if ( IsControl( inputCharacter ) ) { characterType = CharacterType_ControlCharacter; } else if ( IsPunctuation( inputCharacter ) ) { characterType = CharacterType_Punctuation; } else if ( IsDigit( inputCharacter ) ) { characterType = CharacterType_Digit; } else if ( IsLetter( inputCharacter ) ) { characterType = CharacterType_Letter; } Размещайте наиболее вероятные варианты раньше остальных Помес- тив в начало наиболее часто встречающиеся ситуации, вы минимизируете то ко- личество кода, обрабатывающего исключительные случаи, которое придется про- читать при поиске обычных вариантов. Вы увеличите эффективность, потому что уменьшите число проверок, выполняемых кодом в большинстве случаев. В при- веденном примере буквы обычно встречаются чаще, чем знаки пунктуации, но проверка этих знаков написана первой. Вот как исправить код, чтобы буквы про- верялись в первую очередь: Пример проверки прежде всего наиболее часто встречающихся вариантов (C++) Этот случай встречается чаще других, поэтому проверяем его первым. if ( IsLetter( inputCharacter ) ) { characterType = CharacterType_Letter; } > 352 ЧАСТЬ IV Операторы else if ( IsPunctuation( inputCharacter ) ) { characterType = CharacterType_Punctuation; } else if ( IsDigit( inputCharacter ) ) { characterType = CharacterType_Digit; } Этот случай наиболее редкий, поэтому проверяем его последним. else if ( IsControl( inputCharacter ) ) { characterType = CharacterType_ControlCharacter; } Убедитесь, что учтены все варианты Закодируйте в последнем блоке else сообщение об ошибке или утвержде- ние, чтобы отловить ситуации, которые вы не планирова- ли. Это сообщение об ошибке предназначено скорее вам, а не пользователю, так что сформулируйте его соответственно. Вот как модифицировать пример классификации символов для выполнения проверки «других вариантов»: Пример использования варианта по умолчанию для перехвата ошибок (C++) if ( IsLetter( inputCharacter ) ) { characterType = CharacterType_Letter; } else if ( IsPunctuation( inputCharacter ) ) { characterType = CharacterType_Punctuation; } else if ( IsDigit( inputCharacter ) ) { characterType = CharacterType_Digit; } else if ( IsControl( inputCharacter ) ) { characterType = CharacterType_ControlCharacter; } else { DisplayInternalError( “Unexpected type of character detected.” ); } Замените последовательности if-then-else другими конструкциями, кото- рые поддерживает ваш язык программирования Некоторые языки, скажем, Microsoft Visual Basic и Ada, предоставляют операторы case, поддерживающие стро- ки, перечисления и логические функции. Используйте их — они проще в написа- нии и чтении, чем цепочки if-then-else. Код классификации типов символов, на- писанный на Visual Basic с помощью оператора case, может выглядеть так: Пример использования оператора case вместо последовательности if-then-else (Visual Basic) Select Case inputCharacter Case “a” To “z” characterType = CharacterType_Letter Перекрестная ссылка Это так- же хороший пример того, как использовать последователь- ность if-then-else вместо кода глубокой вложенности. Об этой методике см. раздел 19.4. > ГЛАВА 15 Условные операторы 353 Case “ “, “,”, “.”, “!”, “(“, “)”, “:”, “;”, “?”, “-” characterType = CharacterType_Punctuation Case “0” To “9” characterType = CharacterType_Digit Case FIRST_CONTROL_CHARACTER To LAST_CONTROL_CHARACTER characterType = CharacterType_Control Case Else DisplayInternalError( “Unexpected type of character detected.” ) End Select 15.2. Операторы case Оператор case или switch — конструкция, сильно варьирующаяся от языка к язы- ку. C++ и Java поддерживают case только для порядковых типов, рассматривая по одному значению за раз. Visual Basic поддерживает case для порядковых типов и предоставляет мощные средства для обозначения диапазонов и комбинаций зна- чений. Многие языки сценариев вообще не поддерживают case. Следующие разделы представляют основные принципы эффективного использо- вания операторов case. Выбор наиболее эффективного порядка вариантов Организовать порядок следования вариантов в операторе case можно по-разно- му. Если выражение case невелико и содержит только три варианта и три соот- ветствующих строки кода, то выбранный порядок большого значения не имеет. Однако в случае длинного оператора case (скажем, обрабатывающего десятки различных событий в программе, управляемой событиями) порядок следования достаточно важен. Давайте рассмотрим способы упорядочения. Упорядочивайте варианты по алфавиту или численно Если все варианты равнозначны, их размещение в алфавитном порядке улучшает читабельность. В этом случае нужный вариант легко выбрать из группы. Поместите правильный вариант первым Если у вас есть один корректный вариант и несколько исключений, поместите правильное значение первым. От- метьте в комментариях, что этот вариант является нормальным, а все остальные — исключительными. Отсортируйте варианты по частоте Поместите наиболее часто встреча- ющиеся случаи в начало, а более редкие — в конец списка. Этот подход имеет два преимущества. Первое: читатели легко обнаружат ситуации, встречающиеся чаще всего. Второе: при сканировании списка в поисках определенных значений ско- рее всего будут требоваться наиболее часто встречающиеся варианты, поэтому их размещение в начале кода сделает поиск быстрее. Советы по использованию операторов case Далее перечислены советы по применению операторов case. Сделайте обработку каждого варианта простой Код, связанный с каждым вариантом, должен быть корот- Перекрестная ссылка Другие советы по упрощению кода см. в главе 24. 354 ЧАСТЬ IV Операторы ким. Краткость позволяет сделать структуру оператора case прозрачнее. Если дей- ствия, предпринимаемые для какого-то варианта слишком сложны, напишите метод и вызывайте его, а не размещайте весь этот код прямо в блоке case. Не конструируйте искусственные переменные с целью получить возмож- ность использовать оператор case Оператор case следует применять для про- стых данных, которые легко разбить на категории. Если ваши данные нельзя на- звать простыми, используйте цепочки if-then-else. Фальшивые переменные сбива- ют с толки, и их следует избегать. Например, не делайте так: Пример создания искусственной переменной для оператора case — плохая практика (Java) action = userCommand[ 0 ]; switch ( action ) { case ‘c’: Copy(); break; case ‘d’: DeleteCharacter(); break; case ‘f’: Format(); break; case ‘h’: Help(); break; default: HandleUserInputError( ErrorType.InvalidUserCommand ); } Переменная action управляет оператором case. В данном случае action конструи- руется с помощью первого символа строки userCommand, которая вводится пользо- вателем. Этот потенциально опасный код может привести к пробле- мам. Вообще, если вы фабрикуете специальную переменную для case, реальные данные могут не вписываться в данное вы- ражение так, как вы это себе представляете. В приведенном примере, если пользователь набирает copy, в операторе case отрезается первый символ « c», и корректно вызывается ме- тод Copy(). С другой стороны, если пользователь набирает cement overshoes, clambake или cellulite, case точно также отрежет символ « c» и вызовет Copy(). Проверка ошибочных команд в блоке else оператора case не будет нормально функционировать, так как будет обрабатывать только неправильные первые буквы, а не команды целиком. Вместо создания искусственной переменной в этом коде лучше использовать последовательность if-then-else-if и проверять целую строку. Вот как эффективно переписать код: Перекрестная ссылка В проти- воположность этому совету иногда можно улучшить чита- бельность, присваивая значение сложного выражения хорошо названной логической перемен- ной или функции (см. подраз- дел «Упрощение сложных выра- жений» раздела 19.1). |