Инвертировать оператор " if " для уменьшения вложенности

когда я подбежал ReSharper на моем коде, например:

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

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

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

хотелось бы понять, почему это лучше. Я всегда думал, что использование "возврата" в середине метода проблематично, что-то вроде "Гото".

24 ответов


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

double getPayAmount() {
    double result;
    if (_isDead) result = deadAmount();
    else {
        if (_isSeparated) result = separatedAmount();
        else {
            if (_isRetired) result = retiredAmount();
            else result = normalPayAmount();
        };
    }
     return result;
};

В этом случае, если _isDead верно, мы можем сразу выйти из метода. Возможно, было бы лучше структурировать его таким образом:

double getPayAmount() {
    if (_isDead)      return deadAmount();
    if (_isSeparated) return separatedAmount();
    if (_isRetired)   return retiredAmount();

    return normalPayAmount();
};   

Я выбрал этот код из рефакторинг каталог. Этот конкретный рефакторинг называется: замена вложенных условных операторов граничным Статьи.


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

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

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

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


Это немного Религиозный аргумент, но я согласен с ReSharper, что вы должны предпочесть меньше гнездования. Я считаю, что это перевешивает негативы наличия нескольких путей возврата из функции.

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

предпосылки являются отличным примером того, где можно вернуться рано в начале функции. Почему на читаемость остальной части функции должно влиять наличие проверки предварительного условия?

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

наличие нескольких возврат в функции не повлияет на работу программиста обслуживания.

плохая читаемость кода будет.


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

public void myfunction(double exampleParam){
    if(exampleParam > 0){
        //Body will *not* be executed if Double.IsNan(exampleParam)
    }
}

сравните это с казалось бы эквивалентна инверсии:

public void myfunction(double exampleParam){
    if(exampleParam <= 0)
        return;
    //Body *will* be executed if Double.IsNan(exampleParam)
}

Итак, в определенных обстоятельствах то, что кажется правильно перевернутым if может не быть.


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

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

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


Я хотел бы добавить, что есть имя для тех перевернутых предложений if'S - Guard. Я пользуюсь им, когда могу.

Я ненавижу читать код, где есть если в начале, два экрана кода и нет еще. Просто инвертируйте if и вернитесь. Таким образом, никто не будет тратить время на прокрутку.

http://c2.com/cgi/wiki?GuardClause


Это не только влияет на эстетику, но и предотвращает вложенности кода.

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


это, конечно, субъективно, но я думаю, что это сильно улучшает два момента:

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

несколько точек проблема в C (и в меньшей степени C++), потому что они заставляют вас дублировать код очистки перед каждой из точек. С уборкой мусора,try | finally построить и using блоки, на самом деле нет причин, почему вы должны бояться их.

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


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

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

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

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

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


Guard предложения или предварительные условия (как вы, вероятно, можете видеть) проверить, если определенное условие выполняется, а затем нарушает поток программы. Они отлично подходят для мест, где вас действительно интересует только один результат 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();
    }
}

vs:

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

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

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

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


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


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

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


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

например.

 bool PerformDefaultOperation()
 {
      bool succeeded = false;

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

      return succeeded;
 }

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


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

давайте посмотрим на некоторый код 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. если первое условие-false, переходит к коду, где находится второе.
  2. если это правда, переходит к последней инструкции. (Примечание: последняя инструкция-это возврат).
  3. во втором условии то же самое происходит после вычисления результата. Сравните и: добрался до консоли.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. Разница в том, что раньше были прыжки по условию: если false перейти к следующему фрагменту кода, если true перейти к концу. И теперь код IL течет лучше и имеет 3 прыжка (компилятор немного оптимизировал это): 1. Первый прыжок: Когда длина равна 0 до части, где код прыгает снова (третий прыжок) до конца. 2. Во-вторых: в середине второго условия, чтобы избежать одной инструкции. 3. Третье: если второе условие ложно, переходите к концу.

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


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

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

Я не думаю, что вы получите однозначного ответа на этот вопрос.


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

подробнее о предсказании ветвей здесь.


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

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

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

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


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

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


Это спорный вопрос.

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

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


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

Если вы действительно возвращаете returnValue-вложенность, как правило, лучший способ пойти, потому что вы возвращаете returnValue только в одном месте (в конце - duh), и это может сделать ваш код более доступным для обслуживания во многих случаи.


Я думаю, это зависит от того, что вы предпочитаете, как уже упоминалось, нет общего согласия afaik. Чтобы уменьшить раздражение, вы можете уменьшить это предупреждение до "намека"


моя идея заключается в том, что возврат "в середине функции" не должен быть настолько "субъективным". Причина довольно проста, возьмите этот код:

    function do_something( data ){

      if (!is_valid_data( data )) 
            return false;


       do_something_that_take_an_hour( data );

       istance = new object_with_very_painful_constructor( data );

          if ( istance is not valid ) {
               error_message( );
                return ;

          }
       connect_to_database ( );
       get_some_other_data( );
       return;
    }

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


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