Руководство по стилю программирования и конструированию по
Скачать 7.6 Mb.
|
ГЛАВА 19 Общие вопросы управления 427 Упрощение сложных выражений Вы можете предпринять несколько описанных далее шагов для упрощения слож- ных выражений. Разбивайте сложные проверки на части с помощью новых логических переменных Вместо создания чудовищных условий с полудюжиной элементов присвойте значения этих элементов промежуточным переменным, что позволит выполнять упрощенную проверку. Размещайте сложные выражения в логических функциях Если какая-то проверка выполняется неоднократно или отвлекает от основного хода алгорит- ма программы, поместите ее код в отдельную функцию и проверяйте значение этой функции. Вот пример сложного условия: Пример проверки сложного условия (Visual Basic) If ( ( document.AtEndOfStream ) And ( Not inputError ) ) And _ ( ( MIN_LINES <= lineCount ) And ( lineCount <= MAX_LINES ) ) And _ ( Not ErrorProcessing( ) ) Then ‘ Делаем то или иное. End If Это условие выглядит ужасно, и вам приходится его читать, даже если оно вам неинтересно. Поместив его в логическую функцию, вы сможете изолировать эту проверку и позволите читателю забыть о ней, пока она не понадобится. Вот как можно поместить условие if в функцию: Пример сложного условия, помещенного в логическую функцию и использующего для ясности новые промежуточные переменные (Visual Basic) Function DocumentIsValid( _ ByRef documentToCheck As Document, _ lineCount As Integer, _ inputError As Boolean _ ) As Boolean Dim allDataRead As Boolean Dim legalLineCount As Boolean Промежуточные переменные добавлены для упрощения проверки в самой последней строке. allDataRead = ( documentToCheck.AtEndOfStream ) And ( Not inputError ) legalLineCount = ( MIN_LINES <= lineCount ) And ( lineCount <= MAX_LINES ) DocumentIsValid = allDataRead And legalLineCount And ( Not ErrorProcessing() ) End Function Здесь предполагается, что ErrorProcessing() — это некоторая логическая функция, определяющая текущее состояние обработки документа. Теперь, когда вы читае- те основной ход алгоритма, вам не надо разбираться со сложным условием: Перекрестная ссылка О методи- ке использования промежуточ- ных переменных для проясне- ния логических проверок см. подраздел «Используйте логи- ческие переменные для доку- ментирования программы» раз- дела 12.5. > 428 ЧАСТЬ IV Операторы Пример основного хода алгоритма, не содержащего сложное условие (Visual Basic) If ( DocumentIsValid( document, lineCount, inputError ) ) Then ‘ Делаем то или иное. End If Если вы проверяете условие только раз, вам может показаться, что его не стоит помещать в отдельный метод. Но вынесение условия в отдель- ную, хорошо названную функцию улучшит читабельность кода и упрос тит понимание того, что этот код делает. Это достаточная причина для создания функции. Имя нового метода привносит в программу абстракцию, которая доку- ментирует назначение проверки прямо в коде. Это даже лучше, чем документи- рование условия с помощью комментариев, потому что код будет читаться и мо- дифицироваться с большей вероятностью, чем комментарии. Используйте таблицы решений для замены сложных условий Иногда нужно проверять сложные условия, со- держащие несколько переменных. В этом случае для выпол- нения проверки удобней применять таблицы решений, а не операторы if или case. Таблицу решений изначально проще кодировать — она требует пары строк кода и никаких изощренных управляющих структур. Такая ми- нимизация сложности уменьшает возможность ошибок. При изменении данных вы можете изменить таблицу решений, не меняя код: вам всего лишь надо обно- вить содержимое структуры данных. Составление позитивных логических выражений Не немногие люди не имеют проблем с непониманием не- коротких неположительных фраз, т. е. большинство людей имеют трудности с пониманием большого количества от- рицаний. Вы можете предпринять какие-то действия, что- бы избежать кодирования сложных отрицательных логических выражений в про- грамме. В операторах if заменяйте негативные выражения позитивными, меняя местами блоки if и else Вот пример негативного условия: Пример сбивающего с толку отрицательного логического условия (Java) Здесь оператор отрицания. if ( !statusOK ) { // Делаем что-то. } else { // Делаем что-то еще. } Перекрестная ссылка Об ис- пользовании таблиц для заме- ны сложной логики см. главу 18. Я не не нетупой. Гомер Симпсон (Homer Simpson) > ГЛАВА 19 Общие вопросы управления 429 Это условие можно заменить другим, выраженным положительно: Пример более понятного логического условия на Java Условие в этой строке было заменено на противоположное. if ( statusOK ) { // Делаем что-то еще. Код в этом блоке был поменян местами... } else { ...с кодом в этом блоке. // Делаем что-то. } Второй фрагмент кода логически совпадает с первым, но его легче читать, потому что отрицательное выражение было изменено на положительное. В качестве альтернативы вы можете использовать другие имена переменных, которые изменят истинное значение условия на противоположное. В приведенном примере вы можете заменить переменную statusOK на ErrorDetected, ко- торая будет истиной, когда statusOK будет ложно. Применяйте теоремы Деморгана для упрощения логи- ческих проверок с отрицаниями Теоремы Деморгана позволяют эксплуатировать логическую взаимосвязь меж- ду некоторым выражением и версией этого выражения, обозначающего то же са- мое, благодаря использованию двойного отрицания. Рассмотрим фрагмент кода, содержащий следующее условие: Пример условия с отрицанием (Java) if ( !displayOK || !printerOK ) ... Это условие логически эквивалентно следующему: Пример после применения теорем Деморгана (Java) if ( !( displayOK && printerOK ) ) ... В данном случае вам не надо менять местами блоки if и else — выражения в двух последних фрагментах кода логически эквивалентны. Для применения теорем Деморгана к логическому оператору and или or и паре операндов вы инвертиру- ете оба операнда, меняете местами операторы and и or и инвертируете все выра- жение целиком. Табл. 19-1 обобщает возможные преобразования в соответствии с теоремами Деморгана. Перекрестная ссылка Рекомен- дация по составлению положи- тельных логических выражений иногда идет вразрез с рекомен- дацией кодировать номиналь- ный вариант в блоке if, а не в блоке else (см. раздел 15.1). В этом случае вам нужно взвесить преимущества каждого подхода и решить, какой вариант в вашей ситуации будет наилучшим. > > > 430 ЧАСТЬ IV Операторы Табл. 19-1. Преобразования логических переменных в соответствии с теоремами Деморгана Исходное выражение Эквивалентное выражение not A and not B not ( A or B ) not A and B not ( A or not B ) A and not B not ( not A or B ) A and B not ( not A or not B ) not A or not B* not ( A and B ) not A or B not ( A and not B ) A or not B not ( not A and B ) A or B not ( not A and not B ) * Это выражение и используется в примере. Использование скобок для пояснения логических выражений Если у вас есть сложное логическое выражение, не надей- тесь на порядок вычисления его операндов в языке програм- мирования — используйте скобки, чтобы сделать ваши на- мерения понятными. Скобки уменьшают требования, предъ- являемые к читателю кода, который может и не разобрать- ся в тонкостях вычисления логических выражений в вашем языке программирования. Если вы благоразумны, то не будете полагаться на соб- ственную или чью-то еще способность правильно запомнить приоритет вычис- лений, особенно если приходится переключаться между двумя или более языка- ми. Использование скобок не похоже на отправку телеграммы: вы не платите за каждую букву — дополнительные символы бесплатны. Вот пример выражения, содержащего слишком мало скобок: Пример выражения, содержащего слишком мало скобок (Java) if ( a < b == c == d ) ... Начнем с того, что это выражение слишком запутано. Оно тем более сбивает с толку, что не ясно, хотел ли кодировщик проверить условие ( a < b ) == ( c == d ) или ( ( a < b ) == c ) == d. Следующая версия все равно не идеальна, но скобки все же помогают: Пример выражения, частично улучшенного с помощью скобок (Java) if ( ( a < b ) == ( c == d ) ) ... В этом случае скобки повышают удобство чтения и корректность программы, поскольку компилятор не истолковал бы первый фрагмент таким способом. Ког- да сомневаетесь, используйте скобки. Используйте простой метод подсчета для проверки симметричности скобок Если у вас возникают проблемы с поиском парных скобок, то вот про- стой способ подсчета. Начните считать, сказав «ноль». Двигайтесь вдоль выраже- Перекрестная ссылка Примеры применения скобок для прояс- нения других видов выражений см. в подразделе «Скобки» раз- дела 31.2. ГЛАВА 19 Общие вопросы управления 431 ния слева направо. Встретив открывающую скобку, скажите «один». Каждый раз при встрече открывающей скобки уве- личивайте число. А встречая закрывающую скобку, умень- шайте это число. Если к концу выражения у вас опять по- лучится 0, то ваши скобки симметричны. Пример симметричных скобок (Java) Читаем это выражение. if ( ( ( a < b ) == ( c == d ) ) && !done ) ... | | | | | | | | 0 1 2 3 2 3 2 1 0 Произносим эти числа. В этом примере в конце получился 0, следовательно, скобки симметричны. В сле- дующем примере количество отрывающих и закрывающих скобок не одинаково: Пример несимметричных скобок (Java) Читаем это выражение. if ( ( a < b ) == ( c == d ) ) && !done ) ... | | | | | | | 0 1 2 1 2 1 0 -1 Произносим эти числа. Значение 0, полученное до последней закрывающей скобки, подсказывает, что где- то до этой точки была пропущена скобка. Вы не должны получить 0, не достиг- нув последней скобки в выражении. Заключайте в скобки логическое выражение целиком Скобки ничего вам не стоят, а читабельность улучшают. Привычка заключать в скобки все логичес- кое выражение целиком — хорошая практика программирования. Понимание правил вычисления логических выражений Множество языков содержит неявную управляющую форму, которая начинает действовать при вычислении логических выражений. Компиляторы некоторых языков вычисляют каждый элемент логического выражения перед объединением всех этих элементов и вычисления значения всего выражения. Компиляторы других используют «короткозамкнутый» (или «ленивый») алгоритм, обрабатывая только необходимые элементы выражения. Это особенно важно, когда в зависимости от результатов первой проверки вы можете не захотеть выполнять следующий тест. Допустим, вы проверяете элементы массива с помощью следующего выражения: Пример псевдокода неправильной проверки условия while ( i < MAX_ELEMENTS and item[ i ] <> 0 ) ... Если вычисляется выражение целиком, вы получите ошибку при последней ите- рации цикла. В этом случае переменная i равна maxElements, а значит, выражение item[ i ] эквивалентно item[ maxElements ], что является недопустимым значением Перекрестная ссылка Многие текстовые редакторы, ориенти- рованные на программистов, предоставляют команды для поиска парных круглых, квад- ратных и фигурных скобок. О редакторах для программиро- вания см. подраздел «Редакти- рование» раздела 30.2. > > > > 432 ЧАСТЬ IV Операторы индекса. Вы можете возразить, что это не имеет значения, поскольку вы только обращаетесь к элементу, а не изменяете его. Но это неряшество способно сбить с толку читателя вашего кода. Во многих средах этот код будет также генерировать ошибку выполнения или нарушение защиты. Используя псевдокод, можно реструктурировать данное условие так, чтобы эта ошибка не возникала: Пример псевдокода правильно реструктурированной проверки условия while ( i < MAX_ELEMENTS ) if ( item[ i ] <> 0 ) then Этот вариант корректен, так как item[ i ] будет вычисляться, только когда i мень- ше, чем maxElements. Многие современные языки предоставляют средства, которые изначально предот- вращают возможность возникновения такой ошибки. Так, C++ использует корот- козамкнутые вычисления: если значение первого операнда в операции and лож- но, то второй операнд не вычисляется, потому что полное выражение в любом случае будет ложным. Иначе говоря, в C++ единственный элемент выражения: if ( SomethingFalse && SomeCondition ) ... который будет вычисляться, — это SomethingFalse. Обработка выражения заверша- ется, поскольку значение SomethingFalse определяется как ложное. Аналогичное укороченное вычисление будет производиться и для оператора or. В C++ и Java в выражении: if ( somethingTrue || someCondition ) ... вычисляется только somethingTrue. Обработка завершается, как только операнд somethingTrue определяется как истинный, так как все выражение будет истинным, если истинна хотя бы одна из его частей. В результате такого способа вычисле- ния следующее выражение вполне допустимо: Пример условия, которое работает благодаря короткозамкнутому вычислению (Java) if ( ( denominator != 0 ) && ( ( item / denominator ) > MIN_VALUE ) ) ... Если бы это выражение вычислялось целиком, то в случае, когда переменная denominator равна 0, операция деления во втором операнде генерировала бы ошибку деления на 0. Но поскольку вторая часть не вычисляется, если значение первой ложно, то когда denominator равен 0, вторая операция не выполняется, и ошибка деления на 0 не возникает. С другой стороны, из-за того что операция && (and) вычисляется слева направо, следующее логически эквивалентное выражение работать не будет: ГЛАВА 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 ) ... |