Оформление сложных условий
Условный оператор в обычной своей форме источником проблем является сравнительно редко. Однако само условие порой оказывается достаточно сложным и встает на пути к мечте любого разработчика. Речь, конечно же, о красивом и читаемом коде.
Возможно, я не там искал, но ни разу в стандартах оформления кода не встречал упоминаний о том, как быть со сложными условиями. Разобраться с ними — и есть цель данной статьи.
Так как с высасыванием из пальца у меня проблемы, в качестве источника примеров взята часть исходников GCC 4.8.2, для авторов которых стандарты оформления — не пустой звук. Используя примеры, буду приводить файл и строку начала, чтобы желающие могли убедиться, что все честно. Сразу замечу, что, так как примеры реальные и брались из ограниченного источника, некоторые из них могут оказаться не самыми удачными.
В контексте данной статьи сложным будем считать условие, которое состоит из нескольких подусловий и не удовлетворяет требованиям при записи в одну строку. Подразумеваются требования, например, к длине строки или читаемости.
Читаемость, как обычно, определяется на глаз, ибо в одних случаях 3-4 подусловия выделяются при первом же взгляде на участок кода, а в других и с двумя черт ногу сломит. Так, например, использование функций, приведения типов, побитовых операций или вложенности значительно усложняет чтение условий.
Следующий пример находится где-то на грани.
if (LARGEST_EXPONENT_IS_NORMAL (FRAC_NBITS) && (isnan (src) || isinf (src)))
Но порой даже простое условие заставляет на мгновение задуматься.
if ((in.fraction.ll & (((USItype) 1
Естественно, если подобное будет частью условия, то его (результирующее условие) простым назвать трудно.
Примеры с директивами препроцессора внутри условий я намеренно не брал, так как это уже об еще более сложных условиях, выходящих за рамки статьи.
Для улучшения читаемости потребуется разбить условие на блоки. Так же придется поступить, если длина строки с условием превышает допустимую (часто оговаривается в стандартах). Принципы этого разбиения нас и интересуют по двум основным причинам. Во-первых, делать это надо так, чтобы не усугубить ситуацию. Во-вторых, нужно прийти к пусть негласному, но стандарту, ведь сам факт единообразия уже является плюсом к читаемости.
Первое, что приходит на ум, — использование вложенных условных операторов.
if ((((x ^ y) >> I_F_BITS) & 1) == 0) < if (((z ^ x) >> I_F_BITS) & 1) < . >>
if (exp < EXPMAX) if (low >unity || (low == unity && (high & 1) == 1))
Если в первом случае не все так плохо, то во втором вложенное условие близко к тому, что и его необходимо будет дробить. А между тем растет вложенность кода. К тому же в случае с дизъюнкцией такой фокус не пройдет.
Более интересным вариантом являются многострочные условия. Такое решение для некоторых кажется неожиданным, хотя оно много где поддерживается (в тех же C, PHP, Python).
Идея разбиения заключается в том, что на каждой строке оставляется только одно подусловие.
if (!recalc && (isinf (ac) || isinf (bd) || isinf (ad) || isinf (bc)))
Это условие читаемое и легко понимаемое. Но оно не соответствует ранее упомянутому правилу. Здесь всплывает польза однообразия. Если необходимость писать по одному подусловию в строку не оговорена, то анализ этого условия при чтении его усложняется. В обратном случае, даже встретив что-нибудь вроде libgcc/fp-bit.c — 1579 в качестве подусловия, заранее известно, что оно не является сложным.
Большинство многострочных условий в рассматриваемых исходниках все же соответствуют этой идее. Но не ограничиваться же только ею. Отступы в последнем примере подсказывают, что наглядно можно представить и вложенность условий при таком подходе. Единственное, что вызвало сомнение — все подобные условия были слишком уж однообразны, представляли из себя именно такую «лесенку», из-за чего появилась даже мысль о том, что это просто совпадение.
К счастью, нашелся целый один пример, который, без сомнения, подходит под мое определение красивого и понятного сложного условия.
libgcc/libgcov-driver.c — 688:
if (!all_prg->checksum && (cs_all->num != cs_prg->num || cs_all->runs != cs_prg->runs || cs_all->sum_all != cs_prg->sum_all || cs_all->run_max != cs_prg->run_max || cs_all->sum_max != cs_prg->sum_max))
Здесь и соблюден принцип одного подусловия на строку, и наглядно показана вложенность. Разбор такого условия прост, и этим приятен. Представьте его однострочным или в виде трех-четырех вложенных if'ов.
Естественно, совмещать подобное с вложенным условным оператором (как это сделано в libgcc/libgcc2.c — 1611 (примера в статье нет)) не стоит.
Мне на ум приходит еще один вариант реализации данного подхода, но он более громоздкий, и на практике я его не встречал. Что-то вроде следующего:
if ( condition1 && ( condition2 || condition3 ) )
Это лишь один из вариантов. Идея заключается в вынесении закрывающих скобок на отдельную строку. Это избавляет от «скачущих» отступов, как, например, в следующем примере.
libgcc/fixed-bit.c — 1013:
if ((BIG_SINT_C_TYPE) high > (BIG_SINT_C_TYPE) max_high || ((BIG_SINT_C_TYPE) high == (BIG_SINT_C_TYPE) max_high && (BIG_UINT_C_TYPE) low > (BIG_UINT_C_TYPE) max_low)) low = max_low; /* Maximum. */
Ну а расстановка скобок аналогична расстановке всем привычных операторных скобок и потому очевидна и понятна. Скобки для выделения вложенных условий, конечно же, обязательны, так как позволяют избежать ошибок, связанных с приоритетом логических операций.
Думаю, выводы не нужны, каждый для себя сделает сам. Ну а если данный вопрос все же затрагивался в каких-то стандартах или литературе, то ссылки, названия, авторы не помешают (цитаты приветствуются).
UPD: еще один хороший вариант — выделение частей сложного условия в отдельные булевы переменные. Не попалось подходящего рабочего примера, потому не упомянул изначально. За показательный код спасибо lexasss.
bool mustRdraw = (frame.isChanged() || target.isChanged()) || experiment.isRunning(); bool isFullScreen = frame.getSize().equal(screen.getSize()); if (isFullScreen && mustRedraw) < // redraw >
При правильных группировке условий и именовании переменных такой подход несет еще и документирующую функцию.
7.7. Вложенные условные операторы
Когда после ключевых слов then или else вновь используются условные операторы, они называются вложенными. Число вложений может быть произвольно, при этом действует правило: else всегда относится к ближайшему оператору if, для которого ветка else еще не указана. Часто вложением условных операторов можно заменить использование составного.
В качестве примера рассмотрим программу для определения номера координатной четверти p, в которой находится точка с координатами (x,y). Для простоты примем, что точка не лежит на осях координат. Без использования вложений основная часть программы может иметь следующий вид:
Однако использование такого количества условий представляется явно избыточным. Перепишем программу, используя тот факт, что по каждое из условий x>0, x
В первом фрагменте программе проверяется от 2 до 6 условий, во втором -- всегда только 2 условия. Здесь использование вложений дало существенный выигрыш в производительности.
Рассмотренный в п. 7.6 пример с определением знака числа может быть переписан и с использованием вложения:
Однако, как эти операторы, так и составной условный оператор из п. 7.6 проверяют не более 2 условий, так что способы примерно равноценны.
7.8. Оператор выбора
Для случаев, когда требуется выбор одного значения из конечного набора вариантов, оператор if удобнее заменять оператором выбора (переключателем) case:
Оператор выполняется так же, как составной условный оператор.
Выражение должно иметь порядковый тип (целый или символьный). Элементы списка перечисляются через запятую, ими могут быть константы и диапазоны значений того же типа, что тип выражения. Диапазоны указываются в виде:
Оператор диапазона записывается как два рядом стоящих символа точки. В диапазон входят все значения от минимального до максимального включительно.
В качестве примера по номеру месяца m определим число дней d в нем:
Следующий оператор по заданному символу c определяет, к какой группе символов он относится:
else writeln ('Другой символ');
Здесь отдельные диапазоны для русских букв от "а" до "п" и от "р" до "я" связаны с тем, что между "п" и "р" в кодовой таблице DOS находится ряд не-буквенных символов (см. Приложение 1).
Если по ветви оператора case нужно выполнить несколько операторов, действует то же правило, что для оператора if, т. е. ветвь алгоритма заключается в операторные скобки begin . end;.
Вложенные условные операторы в Паскаль
Выбор направления исполнения программы может определяться несколькими условиями. В таких случаях можно использовать вложенные условные операторы или построение сложных условий с помощью логических операций.
Построение сложных условий
Для построения сложных условий в условных операторах применяются логические операции, объединяющие выражения отношения.
Сформулируем правила для построения сложных условий:
- Сложное условие строится с учетом приоритетов логических операций и скобок.
- Операции отношения в языке Паскаль имеют низший приоритет, поэтому в сложном условии они берутся в скобки.
- Если в сложном условии используются операции равного приоритета, то они выполняются последовательно слева направо.
Задача. на вход программе поступает натуральное число. Необходимо выяснить является ли оно двухзначным.
Решение. Очевидно, один из возможных способов решения задачи может быть оформлен путем проверки двух условий, выполняющихся одновременно: х >= 10 и x = 10) and (x then и после else может располагаться только один оператор. Разумеется, этим оператором может быть условный оператор, причем он может располагаться в любой из ветвей исходного оператора. При этом уровень таких вложений неограничен.
Задача. На вход программе поступают три целых числа. Выведите наибольшее из них (программа должна вывести ровно одно число).
Решение. Для решения используем вложенные условные операторы.
- readln (a, b, c);
- if a > b then
- begin
- if a > c then writeln (a)
- else writeln (c)
- end
- else if b > c then writeln (b)
- else writeln (c);
ВНИМАНИЕ: использование краткой формы условного оператора при построении вложенных условных конструкций требует от программиста быть максимально осторожным, поскольку порождает синтаксическую неоднозначность.
Рассмотрим следующую, конструкцию:
- if выражение1 then
- if выражение1 then
- оператор1
- else
- оператор2
Такая запись может быть истолкована двояко с точки зрения принадлежности части else оператор2 первому или второму оператору if . Эта двусмысленность разрешается следующим правилом языка Паскаль: else всегда соответствует первому предшествующему ему оператору if , для которого ветка else еще не указана .
Copyright © 2014-2021, Урок информатики
Все права защищены