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

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

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

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

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

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

Ответ 1

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

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();
};   

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

Ответ 2

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

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

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

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

Ответ 3

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

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

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

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

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

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

Ответ 4

Как отмечали другие, не должно быть производительности, но есть и другие соображения. Помимо этих действительных проблем, это также может открыть вас до получения в некоторых случаях. Предположим, вы имели дело с 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 не может быть.

Ответ 5

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

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

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

Ответ 6

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

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

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

Ответ 7

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

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

Ответ 8

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

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

Ответ 9

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

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

Ответ 10

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

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

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

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

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

Ответ 11

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

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

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

Ответ 12

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

Ответ 13

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

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

Ответ 14

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

например.

 bool PerformDefaultOperation()
 {
      bool succeeded = false;

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

      return succeeded;
 }

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

Ответ 15

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

Посмотрим на некоторый код С# и его скомпилированную форму:


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 выполняет следующие действия:

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

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

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. В-третьих: если второе условие ложно, перейдите в конец.

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

Ответ 16

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

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

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

Ответ 17

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

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

Ответ 18

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

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

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

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

Ответ 19

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

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

Ответ 20

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

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

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

Ответ 21

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

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

Ответ 22

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

Ответ 23

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

    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;
    }

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

Ответ 24

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

Ответ 25

Я не уверен, но я думаю, что R # пытается избежать прыжков в длину. Когда у вас есть IF-ELSE, компилятор делает что-то вроде этого:

Условие false → дальний переход к false_condition_label

true_condition_label: инструкция1... инструкция_н

false_condition_label: инструкция1... инструкция_н

конец блока

Если условие истинно, скачка и развертывания кеша L1 нет, но переход к false_condition_label может быть очень большим, и процессор должен развернуть свой собственный кеш. Синхронизация кеша стоит дорого. R # пытается заменить прыжки в длину на короткие прыжки, и в этом случае существует большая вероятность того, что все инструкции уже находятся в кеше.