Инвертировать оператор if, чтобы уменьшить вложенность - PullRequest
246 голосов
/ 06 ноября 2008

Когда я запустил ReSharper для моего кода, например:

    if (some condition)
    {
        Some code...            
    }

ReSharper дал мне вышеупомянутое предупреждение (оператор Invert "if", чтобы уменьшить вложенность), и предложил следующее исправление:

   if (!some condition) return;
   Some code...

Я бы хотел понять, почему так лучше. Я всегда думал, что использование «возврата» в середине метода проблематично, что-то вроде «goto».

Ответы [ 25 ]

11 голосов
/ 06 ноября 2008

Защитные предложения или предварительные условия (как вы, вероятно, можете видеть) проверяют, выполнено ли определенное условие, и затем нарушают ход программы. Они отлично подходят для мест, где вас действительно интересует только один результат заявления if. Так что вместо того, чтобы сказать:

if (something) {
    // a lot of indented code
}

Вы отменяете условие и нарушаете его, если выполнено это обратное условие

if (!something) return false; // or another value to show your other code the function did not execute

// all the code from before, save a lot of tabs

return далеко не так грязен, как goto. Он позволяет вам передать значение, чтобы показать остальную часть кода, которую не смогла запустить функция.

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

if (something) {
    do-something();
    if (something-else) {
        do-another-thing();
    } else {
        do-something-else();
    }
}

против

if (!something) return;
do-something();

if (!something-else) return do-something-else();
do-another-thing();

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

Я ни на секунду не скажу, что предконфигурации изменят вашу жизнь или уложат вас, но вам может показаться, что ваш код немного проще для чтения.

9 голосов
/ 06 ноября 2008

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

8 голосов
/ 09 декабря 2011

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

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

7 голосов
/ 07 ноября 2008

Лично я предпочитаю только 1 точку выхода. Этого легко добиться, если вы придерживаетесь коротких и точных методов, и это обеспечивает предсказуемый шаблон для следующего человека, который работает над вашим кодом.

например.

 bool PerformDefaultOperation()
 {
      bool succeeded = false;

      DataStructure defaultParameters;
      if ((defaultParameters = this.GetApplicationDefaults()) != null)
      {
           succeeded = this.DoSomething(defaultParameters);
      }

      return succeeded;
 }

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

5 голосов
/ 06 ноября 2008

Множество веских причин о том, как выглядит код . Но как насчет результатов ?

Давайте посмотрим на код C # и его скомпилированную форму IL:


using System;

public class Test {
    public static void Main(string[] args) {
        if (args.Length == 0) return;
        if ((args.Length+2)/3 == 5) return;
        Console.WriteLine("hey!!!");
    }
}

Этот простой фрагмент может быть скомпилирован. Вы можете открыть сгенерированный файл .exe с помощью ildasm и проверить результат. Я не буду публиковать все на ассемблере, но опишу результаты.

Сгенерированный код IL выполняет следующие действия:

  1. Если первое условие ложно, выполняется переход к коду, где находится второе.
  2. Если это правда, переход к последней инструкции. (Примечание: последняя инструкция является возвращением).
  3. Во втором условии то же самое происходит после вычисления результата. Сравните и: попал в Console.WriteLine, если false, или в конец, если это правда.
  4. Распечатать сообщение и вернуться.

Так что, похоже, код будет прыгать до конца. Что делать, если мы делаем нормальный, если с вложенным кодом?

using System;

public class Test {
    public static void Main(string[] args) {
        if (args.Length != 0 && (args.Length+2)/3 != 5) 
        {
            Console.WriteLine("hey!!!");
        }
    }
}

Результаты очень похожи в инструкциях по IL. Разница заключается в том, что раньше выполнялись переходы по условию: если ложь, переходите к следующему коду, если истина, переходите к концу. И теперь код IL работает лучше и имеет 3 перехода (компилятор немного его оптимизировал): 1. Первый переход: когда длина равна 0 до части, где код снова переходит (третий переход) до конца. 2. Второе: в середине второго условия избегать одной инструкции. 3. Третье: если второе условие ложно, переходите к концу.

В любом случае, счетчик программы всегда будет прыгать.

4 голосов
/ 06 ноября 2008

Это просто спорное. Не существует «соглашения между программистами» по вопросу о досрочном возвращении. Насколько я знаю, это всегда субъективно.

Можно сделать аргумент производительности, так как лучше иметь условия, которые написаны, поэтому они чаще всего выполняются; Можно также утверждать, что это яснее. С другой стороны, он создает вложенные тесты.

Не думаю, что вы получите окончательный ответ на этот вопрос.

4 голосов
/ 24 декабря 2011

Теоретически, инвертирование if может привести к повышению производительности, если оно увеличивает частоту предсказания ветвлений. На практике я думаю, что очень трудно точно знать, как будет вести себя прогноз ветвления, особенно после компиляции, поэтому я бы не стал делать это в своей повседневной разработке, кроме случаев, когда я пишу код на ассемблере.

Подробнее о прогнозе ветвления здесь .

3 голосов
/ 14 декабря 2011

Избегание нескольких точек выхода может привести к повышению производительности. Я не уверен насчет C #, но в C ++ оптимизация именованных возвращаемых значений (Copy Elision, ISO C ++ '03 12.8 / 15) зависит от наличия единой точки выхода. Эта оптимизация позволяет избежать создания копии возвращаемого значения (в вашем конкретном примере это не имеет значения). Это может привести к значительному увеличению производительности в тесных циклах, поскольку вы сохраняете конструктор и деструктор при каждом вызове функции.

Но в 99% случаев сохранение дополнительных вызовов конструктора и деструктора не стоит потери читабельности вложенных блоков if (как указывали другие).

3 голосов
/ 14 июня 2011

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

Я обнаружил, что мой код действительно не читается при написании хост-приложения ASIO с ASIOSDK в Steinberg, так как я следовал парадигме вложенности. Он прошел глубиной в восемь уровней, и я не вижу там недостатка дизайна, как упоминал Эндрю Баллок выше. Конечно, я мог бы упаковать некоторый внутренний код в другую функцию, а затем вложить туда оставшиеся уровни, чтобы сделать его более читабельным, но мне это кажется довольно случайным.

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

Так что это может быть другая ситуация, когда инвертированные if могут внести свой вклад в более ясный код.

2 голосов
/ 06 ноября 2008

Это вопрос мнения.

Мой обычный подход состоит в том, чтобы избегать однострочных ifs и возвращаться в середине метода.

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...