Совершенный код. Совершенный код. Мастер-класс. Стив Макконнелл. Руководство по стилю программирования и конструированию по
Скачать 5.88 Mb.
|
ГЛАВА 19 Общие вопросы управления 433 Пример условия, в котором короткозамкнутое вычисление не спасает от ошибки (Java) if ( ( ( item / denominator ) > MIN_VALUE ) && ( denominator != 0 ) ) ... Здесь item / denominator вычисляется раньше, чем denominator != 0. Следователь# но, в этом коде происходит ошибка деления на 0. Язык Java еще более усложняет эту картину, предоставляя «логические» операто# ры. Логические операторы & и | языка Java гарантируют, что все элементы будут вычислены полностью независимо от того, определяется ли истинность или лож# ность выражения без его полного вычисления. Иначе говоря, в Java такое усло# вие будет безопасно: Пример условия, которое работает благодаря короткозамкнутому (условному) вычислению (Java) if ( ( denominator != 0 ) && ( ( item / denominator ) > MIN_VALUE ) ) ... А вот такое — нет: Пример условия, которое не будет работать, потому что короткозамкнутое вычисление не гарантируется (Java) if ( ( denominator != 0 ) & ( ( item / denominator ) > MIN_VALUE ) ) ... Разные языки используют разные способы вычисления, и случается, что разработчики языка чересчур свободно обращаются с правилами вычис# ления выражений, поэтому обратитесь к руководству по вашей версии языка, чтобы выяснить, как в нем выполняются эти операции. Еще лучше (поскольку читатель может не обладать вашей сообразительностью) использовать вложенные условия, проясняющие ваши намерения, и не зависеть от порядка обработки вы# ражений и короткозамкнутых вычислений. Упорядочение числовых выражений в соответствии со значениями на числовой прямой Организуйте числовые условия так, чтобы они соответствовали порядку точек на числовой прямой. В общем случае структурируйте выражения так, чтобы сравне# ния выглядели следующим образом: MIN_ELEMENTS <= i and i <= MAX_ELEMENTS i < MIN_ELEMENTS or MAX_ELEMENTS < i Идея в том, чтобы расположить элементы по порядку слева направо, от наимень# ших к наибольшим. В первой строке MIN_ELEMENTS и MAX_ELEMENTS — это две граничные точки, и поэтому они расположены по краям выражения. Подразуме# вается, что переменная i должна находиться между ними, и поэтому она располо# жена в середине. Во втором примере вы проверяете, находится ли i вне диапазо# на, поэтому i расположена по краям условия, а MIN_ELEMENTS и MAX_ELEMENTS — посредине. Этот подход позволяет легко создать визуальное представление срав# нения (рис. 19#1): 434 ЧАСТЬ IV Операторы Рис. 19'1. Примеры упорядочения логических условий в соответствии с числовой прямой Если вы сравниваете значение i только с MIN_ELEMENTS, то расположение i зави# сит от того, каким должно быть i в случае успешной проверки условия. Если пред# полагается, что i меньше, то условие выглядит так: while ( i < MIN_ELEMENTS ) ... Но если i должно быть больше, вы получаете: while ( MIN_ELEMENTS < i ) ... Этот подход более очевиден, чем такие условия, как: ( i > MIN_ELEMENTS ) and ( i < MAX_ELEMENTS ) которые не позволяют читателю визуализировать проверяемое выражение. Общие принципы сравнения с 0 Языки программирования используют 0 для нескольких целей. Это числовое зна# чение. Это нулевой терминатор в строке. Это пустой указатель. Это значение пер# вого элемента перечисления. Это false в логических выражениях. А посему вам сле# дует писать такой код, чтобы конкретное назначение 0 было очевидно. Неявно сравнивайте логические переменные Как указывалось ранее, целе# сообразно писать логические выражения в виде: while ( !done ) ... Это неявное сравнение с 0 допустимо, потому что выполняется в логическом выражении. Сравнивайте числа с 0 Хотя в логических выражениях неявное сравнение с 0 допустимо, числовые выражения следует сравнивать явно. Для чисел пишите: while ( balance != 0 ) ... а не: while ( balance ) ... ГЛАВА 19 Общие вопросы управления 435 В языке C сравнивайте символы с нулевым терминатором (‘\0’) явно Сим# волы, как и числа, не являются логическими выражениями. Поэтому для симво# лов следует писать: while ( *charPtr != ‘\0’ ) ... а не: while ( *charPtr ) ... Эта рекомендация расходится с широко распространенным соглашением языка C по обработке символьных данных (так, как показано во втором примере). Но первый способ усиливает идею, что выражение имеет дело с символьными, а не с логическими данными. Некоторые соглашения C не основываются на максими# зации удобства чтения или сопровождения, и это — один из таких примеров. К счастью, весь этот вопрос растворяется в лучах заката, поскольку все больше кода пишется с использованием строк C++ и STL. Сравнивайте указатели с NULL Для указателей пишите: while ( bufferPtr != NULL ) ... а не: while ( bufferPtr ) ... Как и в случае с символьными данными, эта рекомендация расходится с устояв# шимся соглашением языка C, но преимущество в читабельности ее оправдывает. Общие проблемы с логическими выражениями Логические выражения содержат еще несколько ловушек, специфичных для не# которых языков программирования: В C'подобных языках, помещайте константы с левой стороны сравнений В C#подобных языках возникают некоторые специфические проблемы с логиче# скими выражениями. Если время от времени вы набираете = вместо ==, подумай# те об использовании соглашения по размещению констант и литералов с левой стороны выражений, например, так: Пример размещения константы с левой стороны выражения на C++. Это позволит компилятору обнаружить ошибку if ( MIN_ELEMENTS = i ) ... В этом выражении компилятор обязан объявить одиночный знак = ошибкой, поскольку недопустимо присваивать какое#нибудь значение константе. А в сле# дующем выражении компилятор, напротив, выдаст только предупреждение, и лишь в том случае, если у вас включены все предупреждения компилятора: Пример размещения константы с правой стороны выражения на C++. Эту ошибку компилятор может и не обнаружить if ( i = MIN_ELEMENTS ) ... 436 ЧАСТЬ IV Операторы Эта рекомендация конфликтует с предложением упорядочивать значения на чис# ловой прямой. Я лично предпочитаю использовать упорядочение, а компилятор пусть предупреждает меня о непреднамеренных присваиваниях. В C++ рассмотрите вопрос подстановки макросов препроцессора вместо операторов &&, || и == (но только как последнее средство) Если у вас воз# никают такие трудности, то можно создать макросы #define для логических опе# раций and и or и использовать AND и OR вместо && и ||. Точно так же очень легко написать =, имея в виду ==. Если вы часто с этим сталкиваетесь, можете создать макрос EQUALS для логического равенства(==). Многие опытные программисты рассматривают этот подход как упрощающий работу тем разработчикам, которые еще не освоились с языком программирова# ния. А тем, кто уже хорошо знает язык, эта методика будет только мешать. Кроме того, многие компиляторы генерируют предупреждающие сообщения, если исполь# зование присваивания и битовых операций выглядит как ошибка. Включение всех предупреждений компилятора часто является лучшим вариантом, чем создание нестандартных макросов. В языке Java учитывайте разницу между a==b и a.equals(b) В Java a==b проверяет, ссылаются ли a и b на один и тот же объект, тогда как a.equals(b) про# веряет, имеют ли объекты одно и то же логическое значение. В общем случае программы на Java должны использовать выражения вроде a.equals(b), а не a==b. 19.2. Составные операторы (блоки) «Составной оператор» или «блок» — это набор операторов, который с точки зре# ния управления ходом программы рассматривается как один оператор. Состав# ные операторы создаются с помощью добавления скобок { и } вокруг группы вы# ражений в языках C++, C#, C и Java. Иногда они задаются с помощью ключевых слов команды, как, например, For и Next в Visual Basic. Пишите обе скобки одновременно Заполняйте внутрен# нее содержимое по#сле того, как напишете открывающую и закрывающую часть блока. Люди часто жалуются, как тя# жело не ошибиться с парными скобками или словами begin# end, хотя это совершенно излишняя проблема. Если вы пос# ледуете этому совету, у вас никогда больше не будет труд# ностей в поиске соответствующих пар скобок. Сначала напишите: for ( i = 0; i < maxLines; i++ ) Потом: for ( i = 0; i < maxLines; i++ ) { } И наконец: for ( i = 0; i < maxLines; i++ ) { // Все что угодно ... } Перекрестная ссылка Многие текстовые редакторы, ориенти- рованные на программиста, со- держат команды для поиска пар- ных круглых, квадратных и фи- гурных скобок (см. подраздел «Редактирование» раздела 30.2). ГЛАВА 19 Общие вопросы управления 437 Это относится ко всем блоковым структурам, включая операторы if, for и while в C++ и Java и сочетания If%Then%Else, For%Next и While%Wend в Visual Basic. Используйте скобки для пояснения условных операторов Довольно слож# но разобраться в коде условного оператора, не разбираясь в действиях, выполня# емых в результате проверки условия if. Размещение единственного оператора после if иногда выглядит эстетически правильным, но в процессе сопровождения такие выражения часто превращаются в более сложные блоки, и одиночный оператор в таких случаях может привести к ошибке. Для пояснения ваших намерений используйте блок независимо от того, сколько в нем строк кода: 1 или 20. 19.3. Пустые выражения В C++ допустимо использования пустого оператора, состоящего исключительно из точки с запятой, например: Пример традиционного пустого выражения (C++) while ( recordArray.Read( index++ ) != recordArray.EmptyRecord() ) ; В C++ после while должно следовать выражение, но оно может быть пустым. От# дельная точка с запятой и является пустым оператором. Далее приведены прин# ципы работы с пустыми выражениями в C++: Привлекайте внимание к пустым выражениям Пус# тые выражения не являются широко распространенными, поэтому сделайте их очевидными. Один из способов — вы# делить точке с запятой, представляющей собой пустой опе# ратор, отдельную строку. Этот подход показан в предыду# щем примере. В качестве альтернативы можно использовать пустые скобки, чтобы подчеркнуть это выражение. Приве# дем два примера: Примеры выделения пустых выражений (C++) Это один из способов выделить пустое выражение. while ( recordArray.Read( index++ ) ) != recordArray.EmptyRecord() ) {} Это еще один способ сделать это. while ( recordArray.Read( index++ ) != recordArray.EmptyRecord() ) { ; } Создайте для пустых выражений макрос препроцессора или встроенную функцию DoNothing() Это выражение не делает ничего, кроме бесспорного под# тверждения того факта, что никакие действия предприниматься не должны. Это похоже на пометку пустых страниц документа фразами «Эта страница намерен# но оставлена пустой». На самом деле страница не совсем пустая, но вы знаете, что ничего другого на ней быть не должно. Перекрестная ссылка Возмож- но, лучший способ обрабаты- вать пустые операторы — это избегать их (см. подраздел «Из- бегайте пустых циклов» разде- ла 16.2). > > 438 ЧАСТЬ IV Операторы Вот как создать собственный пустой оператор в C++ с помощью #define. (Вы так# же можете создать inline#функцию, которая дает тот же эффект.) Пример пустого выражения, выделенного с помощью DoNothing() (C++) #define DoNothing() while ( recordArray.Read( index++ ) != recordArray.EmptyRecord() ) { DoNothing(); } В дополнение к использованию DoNothing() в пустых циклах while и for можно задействовать ее в несущественных вариантах оператора switch — добавление DoNothing() делает очевидным тот факт, что вариант был рассмотрен и никаких действий предприниматься не должно. Если ваш язык не поддерживает макросы препроцессора или встроенные функ# ции, вы можете создать обычный метод DoNothing(), который сразу будет возвра# щать управление вызывающей стороне. Подумайте, не будет ли код яснее с непустым телом цикла Большая часть циклов с пустым телом полагается на побочный эффект в управляющем выраже# нии цикла. В большинстве случаев код будет читабельнее, если эти побочные дей# ствия будут выполняться явно, например: Пример более очевидного цикла с непустым телом (C++) RecordType record = recordArray.Read( index ); index++; while ( record != recordArray.EmptyRecord() ) { record = recordArray.Read( index ); index++; } Этот подход требует дополнительной переменной, управляющей циклом, а так# же большего количества строк, но он делает акцент на простоте программирова# ния, а не на остроумном использовании побочных эффектов. В промышленном коде такой акцент предпочтительней. 19.4. Укрощение опасно глубокой вложенности Чрезмерные отступы (или «вложенность») осуждаются в компьютерной ли тературе уже на протяжении 25 лет и все еще являются главными обви# няемыми в создании запутанного кода. В работах Ноума Чомски и Дже# ральда Вейнберга (Noam Chomsky and Gerald Weinberg) высказывалось предполо# жение, что немногие люди способны понять более трех уровней вложенных if (Yourdon, 1986a), и многие исследователи рекомендуют избегать вложенности, пре# вышающей три или четыре уровня (Myers, 1976; Marca, 1981; Ledgard and Tauer, 1987a). Глубокая вложенность противоречит описанному в главе 5 Главному Тех# ническому Императиву ПО (управлению сложностью). Это достаточная причина для отказа от глубокой вложенности. ГЛАВА 19 Общие вопросы управления 439 Избавиться от глубокой вложенности несложно. Для этого вы можете переписать проверки условий, выполняемые в блоках if и else, или раз# бить код на более простые методы. Упростите вложенные if с помощью повторной проверки части условия Если вложенность становится слишком глубокой, вы можете уменьшить количе# ство ее уровней, повторно проверив некоторые условия. Глубина вложенности в этом примере кода является достаточным основанием для его реструктуризации: Пример плохого, глубоко вложенного кода (C++) if ( inputStatus == InputStatus_Success ) { // Много кода. if ( printerRoutine != NULL ) { // Много кода. if ( SetupPage() ) { // Много кода. if ( AllocMem( &printData ) ) { // Много кода. } } } } Этот пример придуман для демонстрации уровней вложенности. Части, обозна# ченные как // Много кода, подразумевают, что метод содержит достаточно много строк и простирается на нескольких экранах или нескольких страницах напеча# танного листинга. Вот как можно видоизменить этот код, используя повторные проверки, а не вложенность: Пример кода, милосердно избавленного от вложенности с помощью повторных проверок (C++) if ( inputStatus == InputStatus_Success ) { // Много кода. if ( printerRoutine != NULL ) { // Много кода. } } if ( ( inputStatus == InputStatus_Success ) && ( printerRoutine != NULL ) && SetupPage() ) { // Много кода. Перекрестная ссылка Повторная проверка части условия для уменьшения сложности анало- гична повторному тестированию статусной переменной. Такой способ демонстрируется в под- разделе «Обработка ошибок и операторы goto» раздела 17.3. 440 ЧАСТЬ IV Операторы if ( AllocMem( &printData ) ) { // Много кода. } } Это чрезвычайно реалистичный пример, так как показывает, что вы не можете уменьшить уровень вложенности безнаказанно, взамен вам придется формиро# вать более сложный условия. Однако уменьшение с четырех до двух уровней вло# женности дает большое улучшение в читабельности, поэтому такой способ стоит принять во внимание. Упростите вложенные if с помощью блока с выходом Альтернативой к толь# ко что описанному подходу будет создание фрагмента кода, который будет вы# полняться как блок. Если одно из условий в середине блока не выполнится, оста# ток блока будет пропущен. Пример использования блока с выходом (C++) do { // Начало блока с выходом. if ( inputStatus != InputStatus_Success ) { break; // Выходим из блока. } // Много кода. if ( printerRoutine == NULL ) { break; // Выходим из блока. } // Много кода. if ( !SetupPage() ) { break; // Выходим из блока. } // Много кода. if ( !AllocMem( &printData ) ) { break; // Выходим из блока. } // Много кода. } while (FALSE); // Конец блока с выходом Этот способ довольно необычен, поэтому его следует использовать, только если вся ваша команда разработчиков с ним знакома и он одобрен в качестве подхо# дящей практики кодирования. Преобразуйте вложенные if в набор ifthenelse Если вы критически отно# ситесь к вложенными условиями if, вам будет интересно узнать, что вы можете ре# ГЛАВА 19 Общие вопросы управления 441 организовать эти конструкции так, чтобы использовать операторы if%then%else вместо вложенных if. Допустим, у вас есть развесистое дерево решений вроде этого: Пример заросшего дерева решений (Java) if ( 10 < quantity ) { if ( 100 < quantity ) { if ( 1000 < quantity ) { discount = 0.10; } else { discount = 0.05; } } else { discount = 0.025; } } else { discount = 0.0; } Этот фрагмент имеет много недостатков, один из которых в том, что проверяе# мые условия избыточны. Когда вы удостоверились, что значение quantity больше 1000, вам не нужно дополнительно проверять, что оно больше 100 и больше 10. А значит, вы можете преобразовать этот код таким образом: Пример вложенных if, сконвертированных в набор if-then-else (Java) if ( 1000 < quantity ) { discount = 0.10; } else if ( 100 < quantity ) { discount = 0.05; } else if ( 10 < quantity ) { discount = 0.025; } else { discount = 0; } Это решение проще, чем могло бы быть, потому что закономерность увеличения чисел проста. Вот как изменить вложенные if, если бы числа не были так упоря# дочены: Пример вложенных if, преобразованных в набор if-then-else, для случая, когда числа не упорядочены (Java) if ( 1000 < quantity ) { discount = 0.10; } 442 ЧАСТЬ IV Операторы else if ( ( 100 < quantity ) && ( quantity <= 1000 ) ) { discount = 0.05; } else if ( ( 10 < quantity ) && ( quantity <= 100 ) ) { discount = 0.025; } else if ( quantity <= 10 ) { discount = 0; } Основное различие между этим и предыдущим примером в том, что выражения в условиях else%if не полагаются на предыдущие проверки. Этот код не требует вы# полнения блока else, и проверки фактически могут выполняться в любом поряд# ке. Фрагмент мог бы состоять из четырех if и не включать ни одного else. Един# ственная причина, по которой версия с else предпочтительней, — это отказ от ненужных повторных проверок. Преобразуйте вложенные if в оператор case Некоторые виды проверок условий, особенно использующие целые числа, можно перекодировать, применяя оператор case вместо последовательностей if и else. В некоторых языках вы не сможете использовать эту методику, но там, где это возможно, она очень удобна. Вот как преобразовать тот же пример на Visual Basic: Пример преобразования вложенных if к оператору case (Visual Basic) Select Case quantity Case 0 To 10 discount = 0.0 Case 11 To 100 discount = 0.025 Case 101 To 1000 discount = 0.05 Case Else discount = 0.10 End Select Пример читается, как стихи. Если вы сравните его с двумя приведенными ранее — многочисленными отступами, он покажется особенно понятным решением. Факторизуйте глубоко вложенный код в отдельный метод Если глубокая вложенность создается внутри цикла, вы зачастую можете улучшить ситуацию, пе# реместив содержимое цикла в отдельный метод. Это особенно эффективно, если вложенность является результатом как проверок условий, так и итераций. Оставьте блоки if%then%else в основном цикле, чтобы показать ветвление решения, а содер# жимое этих блоков переместите в новые методы. Следующий код нуждается в такой модификации: Пример вложенного кода, требующего разбиения на методы (C++) while ( !TransactionsComplete() ) { // Читаем транзакционную запись. transaction = ReadTransaction(); |