Совершенный код. Совершенный код. Мастер-класс. Стив Макконнелл. Руководство по стилю программирования и конструированию по
Скачать 5.88 Mb.
|
ГЛАВА 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). ГЛАВА 15 Условные операторы 355 Пример использования операторов if-then-else вместо искусственной переменной для оператора case — хорошая практика (Java) if ( UserCommand.equals( COMMAND_STRING_COPY ) ) { Copy(); } else if ( UserCommand.equals( COMMAND_STRING_DELETE ) ) { DeleteCharacter(); } else if ( UserCommand.equals( COMMAND_STRING_FORMAT ) ) { Format(); } else if ( UserCommand.equals( COMMAND_STRING_HELP ) ) { Help(); } else { HandleUserInputError( ErrorType_InvalidCommandInput ); } Используйте вариант по умолчанию только для обработки настоящих значений по умолчанию Порой, когда остается единственный вариант, вы ре# шаете закодировать его как вариант по умолчанию. Заманчиво, но... неразумно: вы лишаетесь автоматического документирования, предоставляемого метками опе# ратора case, и теряете возможность определения ошибок с помощью варианта по умолчанию. Такие операторы case подвержены ошибкам при модификации. Если вы исполь# зуете настоящее значение по умолчанию, то добавление нового варианта триви# ально: вы просто дописываете новую метку и соответствующий код. Если же зна# чение по умолчанию искусственно, провести изменения сложнее. Вам придется добавить новый вариант, возможно, сделав его новым умолчанием, а затем поме# нять прежний вариант по умолчанию так, чтобы он стал обычным вариантом. Поэтому изначально используйте только настоящие значения по умолчанию. Используйте вариант по умолчанию для выявления ошибок Если вариант по умолчанию в операторе case не используется и не планируется для иных дей# ствий, разместите в нем диагностическое сообщение: Пример использования варианта по умолчанию для выявления ошибок — хорошая практика (Java) switch ( commandShortcutLetter ) { case ‘a’: PrintAnnualReport(); break; case ‘p’: // Никаких действий не требуется, но этот вариант предусмотрен. break; case ‘q’: PrintQuarterlyReport(); break; 356 ЧАСТЬ IV Операторы case ‘s’: PrintSummaryReport(); break; default: DisplayInternalError( “Internal Error 905: Call customer support.” ); } Такие сообщения полезны и в отладочном, и в промышленном коде. Большинство пользователей предпочитают сообщения вроде «Внутренняя ошибка: Пожалуйста, позвоните в службу поддержки» отказу системы или, что еще хуже, немного не# верным результатам, которые выглядят вполне нормально до того, как их прове# рит начальник. Если вариант по умолчанию служит не для выявления ошибок, а для иных целей, то подразумевается, что любой выбор будет корректным. Дважды проверьте ваш код и убедитесь, что любое возможное значение на входе оператора case будет допустимо. Если обнаружится какой#то некорректный вариант, перепишите вы# ражения, чтобы условие по умолчанию проверяло ошибки. В C++ и Java старайтесь не писать код, проваливающийся сквозь блоки оператора case C#подобные языки (C, C++ и Java) не выполняют выход из блока case автоматически. Вместо этого вы должны явно закодировать конец блока. Если этого не сделать, программа провалится сквозь этот блок и начнет выполнять следующий. Это иногда приводит к некоторым вопиющим способам программи# рования, включая следующий пример: Пример неправильного использования оператора case (C++) switch ( InputVar ) { case ‘A’: if ( test ) { // оператор 1 // оператор 2 case ‘B’: // оператор 3 // оператор 4 } break; } Этот способ плох, так как приводит к перемешиванию уп# равляющих конструкций. Разобраться во вложенных управ# ляющих конструкциях довольно тяжело, а в перекрываю# щихся — практически невозможно. Модификация вариан# тов ‘A’ или ‘B’ будет посложнее хирургической операции на головном мозге, и вполне вероятно, что их придется пол# ностью переписать, чтобы получить работоспособную вер# сию. Что вы и можете сделать с самого начала. В общем, лучше избегать сквозного перехода в блоках оператора case. Перекрестная ссылка Такое форматирование кода делает пример лучше, чем он есть на самом деле. О том, как сделать, чтобы хороший код выглядел хорошо, а плохой — плохо, см. полдраздел «Разметка концов строк» раздела 31.3 и оставшу- юся часть главы 31. ГЛАВА 15 Условные операторы 357 В C++ ясно и безошибочно обозначайте код, проваливающийся сквозь блоки оператора case Если вы намеренно написали код, который должен выполняться в нескольких блоках case подряд, внятно прокомментируйте место, где это про# исходит, и объясните, почему это должно быть закодировано таким способом: Пример документирования сквозного выполнения блоков case (C++) switch ( errorDocumentationLevel ) { case DocumentationLevel_Full: DisplayErrorDetails( errorNumber ); // СКВОЗНОЙ ПЕРЕХОД — Полная документация также печатает // суммарные комментарии. case DocumentationLevel_Summary: DisplayErrorSummary( errorNumber ); // СКВОЗНОЙ ПЕРЕХОД — Суммарная документация // также печатает номер ошибки. case DocumentationLevel_NumberOnly: DisplayErrorNumber( errorNumber ); break; default: DisplayInternalError( “Internal Error 905: Call customer support.” ); } Эта методика встречается так же часто, как и люди, которые предпочитают по# держанный «Понтиак Ацтек» новенькому «Корвету». Обычно код, переходящий сквозь один блок case к другому, просто напрашивается на ошибки при модифи# кации и его следует избегать. Контрольный список: использование условных операторов Операторы if-then Очевиден ли номинальный путь выполнения кода? Правильно ли выполняется ветвление при проверке if-then на равенство? Присутствует и задокументирован ли блок else? Корректен ли блок else? Правильно ли расположены блоки if и else — нет ли инверсии? Следует ли нормальный вариант после if, а не после else? Последовательности if-then-else-if Преобразуются ли сложные проверки в вызовы логических функций? Проверяются ли наиболее вероятные случаи первыми? Все ли варианты учитываются? Является ли последовательность if-then-else-if лучшей реализацией или лучше использовать оператор case? http://cc2e.com/1545 358 ЧАСТЬ IV Операторы Операторы case Разумно ли отсортированы варианты в операторе case? Сделаны ли действия, выполняемые для каждого варианта, простыми, на- пример, с помощью преобразования в методы в случае необходимости? Проверяет ли оператор case реальную переменную, а не искусственно со- зданную, приводящую к неправильному использованию оператора case? Корректны ли значения, обрабатываемые в блоке по умолчанию? Используется ли блок по умолчанию для выявления ошибок и сообщения о непредвиденных ситуациях? В языках C, C++ или Java содержит ли каждый блок case команды для выхода? Ключевые моменты В простых выражениях if%else обращайте внимание на порядок блоков if и else, особенно если они обрабатывают множество ошибок. Убедитесь, что номиналь# ный вариант прослеживается ясно. Для последовательностей if%then%else и операторов case выбирайте порядок, позволяющий улучшить читабельность. Для перехвата ошибок используйте блок по умолчанию в операторе case или последний блок else в цепочке операторов if%then%else. Управляющие конструкции не равнозначны. Выбирайте конструкцию, наибо# лее подходящую для данного участка кода. ГЛАВА 16 Циклы 359 Г Л А В А 1 6 Циклы Содержание 16.1. Выбор типа цикла 16.2. Управление циклом 16.3. Простое создание цикла — изнутри наружу 16.4. Соответствие между циклами и массивами Связанные темы Укрощение глубокой вложенности: раздел 19.4 Общие вопросы управления: глава 19 Код с условными операторами: глава 15 Последовательный код: глава 14 Отношения между управляющими структурами и типами данных: раздел 10.7 Цикл — это неформальное обозначение любой структуры итеративного типа, т. е. такой, которая заставляет программу повторно выполнять некий блок кода. Наиболее распространенными видами циклов являются for, while и do%while в C++ и Java, For%Next, While%Wend и Do%Loop%While — в Microsoft Visual Basic. Использо# вание циклов — один из наиболее сложных аспектов программирования. Знание, как и когда применять каждый тип цикла, — это решающий фактор в конструи# ровании высококачественного ПО. 16.1. Выбор типа цикла В большинстве языков можно использовать несколько видов циклов, перечислен# ных ниже. Цикл с подсчетом выполняется определенное количество раз, к примеру, один раз для каждого работника. Постоянно вычисляемый цикл не знает заранее, сколько раз он будет выпол# няться и проверяет необходимость завершения при каждой итерации. Напри# мер, он выполняется, пока остаются деньги, пока пользователь не выберет команду завершения или не встретится ошибка. http://cc2e.com/1609 360 ЧАСТЬ IV Операторы Бесконечный цикл выполняется все время с момента старта. Такие циклы можно встретить во встроенных системах, таких как кардиостимуляторы, микровол# новые печи и автопилоты. Цикл с итератором выполняет некоторые действия однократно для каждого элемента контейнерного класса. Первое, чем отличаются эти циклы друг от друга, — это гибкость в определении числа итераций: выполняется ли цикл указанное количество раз или проверяет необходимость завершения на каждом шаге. Кроме того, эти варианты отличаются расположением проверки завершения. Вы можете поместить проверку в начало, середину или конец цикла. Эта характери# стика определяет, будет ли цикл выполняться хоть раз. Если условие цикла про# веряется в начале, то его тело не обязательно будет выполняться. Если цикл про# веряется в конце, то его тело выполняется минимум один раз. Если же проверка находится в середине, то часть цикла, предшествующая ей, выполняется не менее раза, а код, следующий за проверкой, не обязательно будет выполняться. Гибкость и расположение проверки условия определяют тип цикла, выбираемый в качестве управляющей структуры. В табл. 16#1 показаны разновидности циклов в нескольких языках программирования и характеризуются их гибкость и распо# ложение проверки. Табл. 16-1. Типы циклов Язык Тип цикла Гибкость Место проверки Visual Basic For%Next Нет Начало While%Wend Да Начало Do%Loop%While Да Начало или конец For%Each Нет Начало C, C++, C#, Java for Да Начало while Да Начало do%while Да Конец foreach 1 Нет Начало Когда использовать цикл while Новички иногда считают, что условие цикла while проверяется постоянно и цикл завершается в тот момент, когда это условие становится ложным, независимо от того, какой оператор в это время выполняется (Curtis et al, 1986). Хотя цикл while и не столь гибок, он все же является гибким вариантом цикла. Если вы заранее не знаете, сколько итераций должен выполнить цикл, используйте while. Вопреки тому что думают новички, проверка выхода из цикла выполняется только раз за ите# рацию, и главным вопросом в отношении циклов while является выбор места этой проверки — в начале или конце цикла. 1 Реализован только в C#. На момент написания книги планируется в других языках, включая Java. ГЛАВА 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 При использовании этого типа циклов учитывайте следующие тонкие моменты. > > |