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

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

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

Так как с высасыванием из пальца у меня проблемы, в качестве источника примеров взята часть исходников 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
}

При правильных группировке условий и именовании переменных такой подход несет еще и документирующую функцию.

Similar posts

Ads
AdBlock has stolen the banner, but banners are not teeth — they will be back

More

Comments 33

    +23
    В моей практике сложное условие часто разбивается на несколько простых, под результат которых выделяются переменные, и в if идут только эти переменные. Например (код искусственный):

    bool mustRdraw = (frame.isChanged() || target.isChanged()) || experiment.isRunning();
    bool isFullScreen = frame.getSize().equal(screen.getSize());
    if (isFullScreen && mustRedraw) {
      // redraw
    }
    

    Не знаю, согласуется ли такой подход с тем что советуют профессионалы, но субъективно — так легче читаются на лету составные части условия.
      +1
      Иногда проверки должны идти в строго заданном порядке и если одна будет ложной, то следующие проверять нельзя. Тогда подход с переменными становится бесполезным и приходится городить либо вложенные условия, либо сложные, а так – полезная практика.
        0
        В C# я часто для этого использовал функции
        Func<bool> mustDraw = () => (frame.isChanged() || target.isChanged()) || experiment.isRunning();
        Func<bool> isFullScreen = () => frame.getSize().equal(screen.getSize());
        if (isFullScreen() && mustRedraw()) {
          // redraw
        }
        

          0
          У Вас в коде ошибка ;-)
          Пример того, что такие методики тоже могут дать логическую ошибку на ровном месте.
          И в верхнем примере тоже ошибка, вызванная опечаткой.
        +1
        мне тоже нравится такой подход, отчасти тем, что значения переменных легко посмотреть в дебаггере
          +1
          bool mustRdraw = (frame.isChanged() || target.isChanged()) || experiment.isRunning();
          bool isFullScreen = frame.getSize().equal(screen.getSize());
          if (isFullScreen && mustRedraw) {
            // redraw
          }
          


          Допустим, что где-то вверху объявлена bool mustRedraw…
          И… Бум!!!
          +2
          Для себя выработал правило Yoda conditions + автоматическое форматирование + выделение сложных условий в функции или переменные. Потому что, честно говоря, эти отступы и рюшки хороши пока у кода один хозяин — да и то не долго. Каждое изменение такого условия порождает переформатирование, что бы красиво было… Это уже ASCII Art, получается, а не программирование.
            +1
            Я использую для таких условий нечто вроде:

            if ( true
              && condition1
              && ( false
                || condition_2_1
                || condition_2_2
              )
            ) {
              // do something
            }
            
            
              +4
              Поступаю аналогично. Очень удобно построчно переносить код а также использовать условную компиляцию. Особенно такие конструкции разрастаются в sql (при этом true заменяется на 1=1). А для сложных случаев в комментариях описывается сначала таблица истинности, по которой, собственно, и составляется условие.
                –1
                Спасибо. Красиво, и почти соответствует последнему примеру.
                Но, наверно, лучше, если по команде редактор покажет код условия в виде дерева, да еще и посоветует минимальную группировку выражения
                  0
                  А true и false просто для того, чтобы не оставлять пустые открывающие скобки? Получается же и избыточность, и увеличение сложности. Стоит ли оно того?
                    0
                    Предлагаю решать самостоятельно.

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

                    И да, speakingfish прав, — в SQL это еще более удобно (при генерации WHERE, например).
                    +2
                    Не совсем удобно. Любой мысль в коде должна быть или завершенной полностью, к примеру вызов метода с его аргументами на одной строке, либо явно намекать на то что «to be continue», т.е. ищи продолжение. Когда вы пишите && связывая под-условия, лучше всего это делать на одной строке, а продолжение мысли, т.е. написание след. под-условия на второй. Потому что когда Вы смотрите:

                    if ( file.Exists() &&

                    то Вы сразу понимаете, что мысль не завершена! Следовательно надо читать мысль(условие) далее!

                    И того Ваш кусок кода, при использовании моего предложения будет выглядеть так:

                    if ( true &&
                    condition1 &&
                    ( false ||
                    condition_2_1 || condition_2_2 )
                    ) {
                    // do something
                    }

                    Но я бы сделал так:

                    bool subCondition1 = true && condition1;
                    bool subCondition2 = false || (condition_2_1 || condition_2_2);
                    if (subCondition1 && subCondition2 ) {
                    // more code
                    }

                    P.S.: Сорри за то что не применяю тег код, он недоступен на хабре тем у кого карма ниже нуля.
                      0
                      C предварительным вычислением условий есть небольшая засада.

                      Иногда вычисление какого-то условия является «тяжелой» операцией и часто не требуется.

                      Если мы расписываем условия в одном if, без предвычислений, то за счет lazy ops такие вычисления можно пропустить, что не всегда возможно при предвычислениях.
                        0
                        1) Преждевременная оптимизация, что?
                        2) Никто не отменял составлять под-условия по их наиболее вероятному состоянию.
                        • UFO just landed and posted this here
                            0
                            Тогда выделять можно не в переменную, а в функцию, в которой в зависимости от наличия нулевого вектора будет либо сразу возвращаться результат, либо происходить дальнейшее вычисление результата с учетом косинуса. Вот только такими темпами можно одно условие до небольшой библиотеки раздуть.
                        +1
                        И в вашем стиле ставить true или false перед остальными условиями бессмысленно. Вам подошла запись вида

                        if (
                          condition1 &&
                          (
                            condition2_1 ||
                            condition2_2 ||
                            false
                          ) &&
                          true
                        ) {
                          // do something
                        }
                        


                        Но в таком виде ни у кого не видел :)
                          +3
                          Возможно, глупый вопрос, но зачем в таком варианте вообще true и false?
                            +1
                            Для единообразной записи условий, например. В таком виде условия можно копировать, перемещать, добавлять и удалять всегда в одной и той же манере, вне зависимости от их расположения.
                              –2
                              Не аргумент! Т.к. вызывает вопросы, товарищь gwer совсем не глупый вопрос задал!
                                0
                                Некоторые явления кажутся нам очевидными лишь из-за того, что они повсеместно используются.

                                Я задал вопрос потому, что встретил сие впервые (осмысленно). Но это действительно плюс для форматированных условий.

                                С другой стороны, так ли часто это дает какой-либо достаточно весомый выигрыш для того, чтобы ради этого и без того сложное условие загромождать новыми элементами?
                                  0
                                  Поверьте — условия, записанные таким образом, читаются легче. Ведь вся разница только в наличии true/false при открывающей скобке, — а в остальном все очень даже мимими.

                                  Просто попробуйте в своем коде так переформатировать пару if.
                                    0
                                    Смотрится убого! Полезность нулевая. Посмотрел в коде Linux и FreeBSD нигде подобной записи условий не увидел. Видимо для прикладников это имеет значение, а вот что-то системщики что-то не жалуют или я плохо искал пример, тогда прошу дать ссылку на сорец из реальных боевых программ прошедших испытание временем
                      +2
                      На мой взгляд, если изменить style guide и вместо записи
                      isnan (src) || isinf (src))

                      записть
                      isnan(src) || isinf(src))
                      ,
                      то визуально это будет выглядет как две лексемы, а не четыре, и читать будет легче.
                        0
                        Согласен, пробелы очень сильно влияют на читаемость. Аналогичная ситуация, например, с приведением типов и с отрицаниями. Приведение в примерах есть, а вот отрицание в выборку интересных условий попало, но до статьи не дошло.

                        libgcc/libgcc2.c — 1611:
                        if (! (- ((DWtype) 1 << FSIZE) < u
                               && u < ((DWtype) 1 << FSIZE)))
                          {
                            if ((UDWtype) u & (REP_BIT - 1))
                              {
                                u &= ~ (REP_BIT - 1);
                                u |= REP_BIT;
                              }
                          }	
                        

                        Пробелы эти, на мой взгляд, ни к чему. Кстати, это еще один вариант совокупности описанных решений. Многострочное условие вкупе с вложенным условным оператором.
                        +1
                        >>Возможно, я не там искал, но ни разу в стандартах оформления кода не встречал упоминаний о том, как быть со сложными условиями.
                        Да, не там! Стив Макконнелл «Совершенный код». Также есть книга «Чистый код» от Мартина и там тоже это все описывается! Эти книги «must read», если Вы еще не читали, то настоятельно рекомендую, хоть 1% полезности из них но возьмете!
                          0
                          Макконнелл предлагал разбивать условие на переменные или функции. Это хороший подход. Но группируемые подусловия должны быть логично связаны, то есть вводимая функция/переменная является эдаким слоем абстракции. Но вдруг условие требуется очень сложное, и получилось что-то из следующего списка:
                          • Условие состоит из большого числа слабо связанных подусловий, которые затруднительно логично и красиво разбить.
                          • Мы выделили функции/переменные, но их много, и даже с ними условие воспринимается не очень хорошо.
                          • Мы выделили функции/переменные, конечное условие красивое и наглядное, но в функциях/переменных теперь сложные условия.

                          Или еще какая-то ситуация, когда нужно написать сложное условие. Как поступим?

                          К слову, в примере Макконнелла на эту тему сложное условие, разбитое на три строки, которое он называет ужасным, на мой взгляд, выглядит лучше и понятнее, нежели то, что он нагородил с функцией и переменными.

                          А вот у Мартина не помню упоминаний этой темы. Процитируете?
                            +2
                            В разделе «Функции» Мартин просто в своём примере заменяет условие на функцию, никак на этом особо не заостряя внимание.

                            А вот Мартин Фаулер в «Рефакторинге» как раз вводит понятия «Декомпозиция условного оператора» и «Консолидация условного оператора», которые связаны с выделением условий в функции. В книге это всё описано в целом разделе «Упрощение условных выражений», в общем, есть что почитать в рамках данного вопроса
                              0
                              Да, вы правы, он приводил пример! Я же программист, когда читаю техническую книгу то отлично понимаю что мне надо не только представлять что происходит, но и почему! Т.е. «видеть код»!!! Поэтому для меня любой пример, почти что «абзац написанный словами». Бывают хорошие «абзацы», а бывают такие что следует взять на заметку! Да и люблю читать чужие исходные код, в конечном итоге многому учишься сам того не осознавая )
                          +2
                          В продолжение темы — еще есть проблема вложенных многоуровневых условий.
                          С этим метод борьбы простой. Надо вместо
                          if(a) {
                              if(b) {
                                  op1;
                                  if(c) op2;
                                  op3;
                              }
                          } else {
                            if(!c) {
                              op4;
                            }
                          }
                          

                          писать
                          if(a && b) op1;
                          if(a && b && c) op2;
                          if(a && b) op3;
                          if(!a && !c) op4;
                          

                          Не помню точно первоисточника, но вроде бы это было еще у Дейкстры.
                            +1
                            У Мартина Фаулера в «Рефакторинге» это называется «Замена вложенных условных операторов граничным оператором»
                            0
                            Сам я тоже пришел к стилю как в примере выше в «libgcc/libgcov-driver.c — 688:», кажется самым наглядным из всех.

                            Only users with full accounts can post comments. Log in, please.