Как стать автором
Обновить

Оформление сложных условий

Время на прочтение4 мин
Количество просмотров69K
Условный оператор в обычной своей форме источником проблем является сравнительно редко. Однако само условие порой оказывается достаточно сложным и встает на пути к мечте любого разработчика. Речь, конечно же, о красивом и читаемом коде.

Возможно, я не там искал, но ни разу в стандартах оформления кода не встречал упоминаний о том, как быть со сложными условиями. Разобраться с ними — и есть цель данной статьи.

Так как с высасыванием из пальца у меня проблемы, в качестве источника примеров взята часть исходников GCC 4.8.2, для авторов которых стандарты оформления — не пустой звук. Используя примеры, буду приводить файл и строку начала, чтобы желающие могли убедиться, что все честно. Сразу замечу, что, так как примеры реальные и брались из ограниченного источника, некоторые из них могут оказаться не самыми удачными.

В контексте данной статьи сложным будем считать условие, которое состоит из нескольких подусловий и не удовлетворяет требованиям при записи в одну строку. Подразумеваются требования, например, к длине строки или читаемости.

Читаемость, как обычно, определяется на глаз, ибо в одних случаях 3-4 подусловия выделяются при первом же взгляде на участок кода, а в других и с двумя черт ногу сломит. Так, например, использование функций, приведения типов, побитовых операций или вложенности значительно усложняет чтение условий.

Следующий пример находится где-то на грани.

libgcc/fp-bit.c — 205:
if (LARGEST_EXPONENT_IS_NORMAL (FRAC_NBITS) && (isnan (src) || isinf (src)))

Но порой даже простое условие заставляет на мгновение задуматься.

libgcc/fp-bit.c — 1579:
if ((in.fraction.ll & (((USItype) 1 << F_D_BITOFF) - 1)) != 0)

Естественно, если подобное будет частью условия, то его (результирующее условие) простым назвать трудно.

Примеры с директивами препроцессора внутри условий я намеренно не брал, так как это уже об еще более сложных условиях, выходящих за рамки статьи.

Для улучшения читаемости потребуется разбить условие на блоки. Так же придется поступить, если длина строки с условием превышает допустимую (часто оговаривается в стандартах). Принципы этого разбиения нас и интересуют по двум основным причинам. Во-первых, делать это надо так, чтобы не усугубить ситуацию. Во-вторых, нужно прийти к пусть негласному, но стандарту, ведь сам факт единообразия уже является плюсом к читаемости.

Первое, что приходит на ум, — использование вложенных условных операторов.

libgcc/fixed-bit.c — 84:
if ((((x ^ y) >> I_F_BITS) & 1) == 0)
  {
    if (((z ^ x) >> I_F_BITS) & 1)
      {
        ...
      }
  }

libgcc/fp-bit.c — 361:
if (exp < EXPMAX)
	if (low > unity || (low == unity && (high & 1) == 1))
	{
		...
	}

Если в первом случае не все так плохо, то во втором вложенное условие близко к тому, что и его необходимо будет дробить. А между тем растет вложенность кода. К тому же в случае с дизъюнкцией такой фокус не пройдет.

Более интересным вариантом являются многострочные условия. Такое решение для некоторых кажется неожиданным, хотя оно много где поддерживается (в тех же C, PHP, Python).

Идея разбиения заключается в том, что на каждой строке оставляется только одно подусловие.

libgcc/libgcc2.c — 1980
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
}

При правильных группировке условий и именовании переменных такой подход несет еще и документирующую функцию.
Теги:
Хабы:
Всего голосов 25: ↑17 и ↓8+9
Комментарии33

Публикации

Истории

Ближайшие события

22 – 24 ноября
Хакатон «AgroCode Hack Genetics'24»
Онлайн
28 ноября
Конференция «TechRec: ITHR CAMPUS»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань