Должна ли функция иметь только один оператор возврата?

780

Есть ли веские причины, по которым лучше иметь в функции только один оператор return?

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

Tim Post
источник
25
Я не согласен с тем, что вопрос не зависит от языка. С некоторыми языками иметь несколько возвратов более естественно и удобно, чем с другими. Я бы с большей вероятностью пожаловался на ранние результаты в функции C, чем в C ++, использующей RAII.
Адриан Маккарти
3
Это тесно связано и имеет отличные ответы: programmers.stackexchange.com/questions/118703/…
Тим Шмельтер
от языка? Объясните человеку, использующему функциональный язык, что он должен использовать один возврат для каждой функции: p
Boiethios

Ответы:

741

У меня часто есть несколько утверждений в начале метода, чтобы вернуться к «легким» ситуациям. Например, это:

public void DoStuff(Foo foo)
{
    if (foo != null)
    {
        ...
    }
}

... можно сделать более читабельным (ИМХО) вот так:

public void DoStuff(Foo foo)
{
    if (foo == null) return;

    ...
}

Так что да, я думаю, что хорошо иметь несколько «точек выхода» из функции / метода.

Мэтт Гамильтон
источник
83
Согласовано. Хотя наличие нескольких точек выхода может выйти из-под контроля, я определенно думаю, что это лучше, чем поместить всю вашу функцию в блок IF. Используйте return так часто, как это имеет смысл, чтобы ваш код читался.
Джошуа Кармоди
172
Это известно как «охранное заявление» - это Рефакторинг Фаулера.
Ларс Вестергрен
12
Когда функции сохраняются относительно короткими, нетрудно проследить структуру функции с точкой возврата около середины.
KJAWolf
21
Огромный блок операторов if-else, каждый с возвратом? Это ничего. Такая вещь обычно легко подвергается рефакторингу. (По крайней мере, более распространенным вариантом с одним выходом является переменная результата, поэтому я сомневаюсь, что вариант с несколькими выходами будет более трудным.) Если вы хотите по-настоящему головной боли, посмотрите на комбинацию циклов if-else и while (контролируемых локальным логическое значение), где устанавливаются переменные результата, приводящие к конечной точке выхода в конце метода. Это идея единственного выхода, которая сошла с ума, и да, я говорю по фактическому опыту, с которым пришлось столкнуться.
Маркус Андрэн
7
'Представьте себе: вам нужно сделать метод IncreaseStuffCallCounter в конце функции' DoStuff '. Что вы будете делать в этом случае? :) '-DoStuff() { DoStuffInner(); IncreaseStuffCallCounter(); }
Джим Балтер
355

Никто не упомянул или не процитировал Code Complete, поэтому я сделаю это.

17.1 возврат

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

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

Chris S
источник
64
+1 за нюанс «минимизировать», но не запрещать множественные возвраты.
Raedwald
13
«труднее понять» очень субъективно, особенно когда автор не предоставляет каких-либо эмпирических доказательств в поддержку общего утверждения ... наличие единственной переменной, которая должна быть установлена ​​во многих условных местах в коде для окончательного оператора return, одинаково подвергается «не подозревая о возможности того, что переменная была назначена где-то выше функции»
Хестон Т. Хольтманн,
26
Лично я предпочитаю возвращаться рано, где это возможно. Почему? Что ж, когда вы видите ключевое слово return для данного случая, вы мгновенно узнаете «Я закончил» - вам не нужно продолжать читать, чтобы выяснить, что, если что-нибудь произойдет, потом.
Марк Симпсон
12
@ HestonT.Holtmann: Дело в том, что сделал Совершенный код уникален среди книг по программированию в том , что совет является подкреплены эмпирическими данными.
Адриан Маккарти
9
Вероятно, это должен быть принятый ответ, поскольку упоминается, что наличие нескольких точек возврата не всегда хорошо, но иногда необходимо.
Рафид
229

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

string fooBar(string s, int? i) {
  string ret = "";
  if(!string.IsNullOrEmpty(s) && i != null) {
    var res = someFunction(s, i);

    bool passed = true;
    foreach(var r in res) {
      if(!r.Passed) {
        passed = false;
        break;
      }
    }

    if(passed) {
      // Rest of code...
    }
  }

  return ret;
}

Сравните это с кодом , где несколько точек выхода являются разрешенными: -

string fooBar(string s, int? i) {
  var ret = "";
  if(string.IsNullOrEmpty(s) || i == null) return null;

  var res = someFunction(s, i);

  foreach(var r in res) {
      if(!r.Passed) return null;
  }

  // Rest of code...

  return ret;
}

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

ljs
источник
12
Ясность в глазах смотрящего - я смотрю на функцию и ищу начало середину и конец. Когда функция маленькая, это хорошо - но когда вы пытаетесь понять, почему что-то сломалось, и «Остаток кода» оказывается нетривиальным, вы можете потратить целую вечность на поиски, почему это так
Murph
7
Прежде всего, это надуманный пример. Во-вторых, где: строка ret; "перейти во вторую версию? В-третьих, Ret не содержит полезной информации. В-четвертых, почему так много логики в одной функции / методе? В-пятых, почему бы не сделать DidValuesPass (тип res), а затем RestOfCode () отдельным подфункции?
Рик Минерих
25
@Rick 1. Не придуманный в моем опыте, это - образец, с которым я сталкивался много раз, 2. Он назначен в «остальной части кода», возможно, это не ясно. 3. Гм? Это пример? 4. Ну, я думаю, что это выдумано в этом отношении, но это можно оправдать, 5. может сделать ...
ljs
5
@ В том-то и дело, что часто легче вернуться раньше, чем заключить код в большой оператор if. По моему опыту, это очень важно, даже при правильном рефакторинге.
LJS
5
Смысл отсутствия множественных операторов возврата состоит в том, чтобы упростить отладку, а на секунду - для удобства чтения. Одна точка останова в конце позволяет вам увидеть значение выхода, без исключений. Что касается читабельности, 2 показанные функции не ведут себя одинаково. Возвращаемым по умолчанию является пустая строка, если! R.Passed, но «более читабельная» меняет это на нулевое. Автор неправильно прочитал, что раньше было всего несколько строк по умолчанию. Даже в тривиальных примерах легко иметь нечеткие возвраты по умолчанию, что-то, что может помочь принудительное выполнение одного возврата в конце.
Митч
191

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

С теорией «единой точки выхода» вы неизбежно получите код, который выглядит следующим образом:

function()
{
    HRESULT error = S_OK;

    if(SUCCEEDED(Operation1()))
    {
        if(SUCCEEDED(Operation2()))
        {
            if(SUCCEEDED(Operation3()))
            {
                if(SUCCEEDED(Operation4()))
                {
                }
                else
                {
                    error = OPERATION4FAILED;
                }
            }
            else
            {
                error = OPERATION3FAILED;
            }
        }
        else
        {
            error = OPERATION2FAILED;
        }
    }
    else
    {
        error = OPERATION1FAILED;
    }

    return error;
}

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

Этот метод делает обслуживание кода чрезвычайно сложным и подверженным ошибкам.

17 из 26
источник
5
@ Murph: у вас нет возможности узнать, что ничего не происходит после каждого условия, не читая каждое из них. Обычно я бы сказал, что такого рода темы субъективны, но это явно неправильно. С возвратом каждой ошибки, вы сделали, вы точно знаете, что произошло.
GEOCHET
6
@Murph: я видел такой код, который использовали, злоупотребляли и злоупотребляли. Пример довольно прост, так как нет истинного if / else внутри. Все, что должен сломать этот вид кода, - это одно «другое» забытое. AFAIK, этот код очень нуждается в исключениях.
paercebal
15
Вы можете реорганизовать его в это, сохранив его «чистоту»: if (! SUCCEEDED (Operation1 ())) {} else error = OPERATION1FAILED; if (error! = S_OK) {if (SUCCEEDED (Operation2 ())) {} else error = OPERATION2FAILED; } if (error! = S_OK) {if (SUCCEEDED (Operation3 ())) {} else error = OPERATION3FAILED; } //так далее.
Джо Пинеда
6
Этот код не имеет только одной точки выхода: каждый из этих операторов "error =" находится на пути выхода. Это не просто выход из функций, это выход из любого блока или последовательности.
Джей Базузи
6
Я не согласен с тем, что одиночное возвращение «неизбежно» приводит к глубокой вложенности. Ваш пример может быть записан как простая линейная функция с одним возвратом (без gotos). И если вы не можете или не можете полагаться исключительно на RAII для управления ресурсами, то раннее возвращение в конечном итоге приводит к утечкам или дублированию кода. Самое главное, что раннее возвращение делает непрактичным утверждение постусловий.
Адриан Маккарти
72

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

cdv
источник
44
Структурированное программирование ничего не говорит. Некоторые (но не все) люди, которые называют себя сторонниками структурного программирования, говорят это.
Внуаз
15
«Это хорошо работает, если вы следуете его другим советам и пишете небольшие функции». Это самый важный момент. Маленькие функции редко требуют много точек возврата.
6
@wnoise +1 за комментарий, так что правда. Все "программирование структуры" говорит, что не используйте GOTO.
paxos1977
1
@ceretullis: если это не нужно. Конечно, это не существенно, но может быть полезно в C. Ядро Linux использует его, и по уважительным причинам. GOTO считал, что Harmful говорил об использовании GOTOдля перемещения потока управления, даже когда функция существовала. Он никогда не говорит "никогда не использовать GOTO".
Эстебан Кюбер
1
«все о игнорировании« структуры »кода.» - Нет, скорее наоборот. «имеет смысл сказать, что их следует избегать» - нет, это не так.
Джим Балтер
62

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

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

Я нахожу функцию, написанную с защитными предложениями, намного более легкой, чем одна длинная вложенная группа if then elseутверждений.

пустой
источник
Конечно, «одна длинная вложенная группа операторов if-then-else» - не единственная альтернатива охранным предложениям.
Адриан Маккарти
@AdrianMcCarthy у тебя есть лучшая альтернатива? Это было бы более полезно, чем сарказм.
Синдзоу
@kuhaku: Я не уверен, что назвал бы это сарказмом. Ответ предполагает, что это либо / или ситуация: либо пункты охраны, либо длинные вложенные связки if-then-else. Многие (большинство?) Языков программирования предлагают множество способов для учета такой логики, кроме защитных предложений.
Адриан Маккарти
61

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

Другими словами, по возможности, поддерживайте этот стиль

return a > 0 ?
  positively(a):
  negatively(a);

через это

if (a > 0)
  return positively(a);
else
  return negatively(a);

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

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

Apocalisp
источник
6
+1 «Если вы обнаружите, что ваши ifs и elses синтаксически далеко друг от друга, вы можете разбить это на более мелкие функции».
Андрес Яан Тэк
4
+1, если это проблема, это обычно означает, что вы делаете слишком много в одной функции. Меня действительно огорчает, что это не самый высоко оцененный ответ
Мэтт Бриггс
1
Охранные заявления также не имеют побочных эффектов, но большинство людей сочли бы их полезными. Так что могут быть причины прекратить выполнение на ранней стадии, даже если побочные эффекты отсутствуют. Этот ответ не полностью решает проблему, по моему мнению.
Maarten Bodewes
@ MaartenBodewes-owlstead См. «
Apocalisp
43

Я видел это в стандартах кодирования для C ++, которые были пережитком C, как если бы у вас не было RAII или другого автоматического управления памятью, тогда вы должны очищать для каждого возврата, что либо означает вырезать и вставить очистки или перехода (логически то же самое, что «наконец» в управляемых языках), оба из которых считаются дурным тоном. Если вы практикуете использование умных указателей и коллекций в C ++ или другой автоматической системе памяти, то для этого нет веских оснований, и все сводится к удобочитаемости и, скорее, к суждению.

Pete Kirkham
источник
Хорошо сказано, хотя я действительно считаю, что лучше копировать удаляемые файлы при попытке написать высокооптимизированный код (например, программные скины сложных трехмерных сеток!)
Грант Питерс
1
Что заставляет вас в это верить? Если у вас есть компилятор с плохой оптимизацией, у которого есть некоторые издержки при разыменовании auto_ptr, вы можете использовать простые указатели параллельно. Хотя было бы странно писать «оптимизированный» код с неоптимизирующим компилятором.
Пит Киркхэм
Это делает интересное исключение из правила: если ваш язык программирования не содержит чего-то, что автоматически вызывается в конце метода (такого как try... finallyв Java), и вам нужно выполнить обслуживание ресурсов, которое вы могли бы сделать с одним возврат в конце метода. Прежде чем сделать это, вы должны серьезно подумать о рефакторинге кода, чтобы избавиться от ситуации.
Maarten Bodewes
@PeteKirkham, почему goto для очистки плох? да, goto может использоваться плохо, но это конкретное использование не плохо.
q126y
1
@ q126y в C ++, в отличие от RAII, происходит сбой при возникновении исключения. В Си это совершенно допустимая практика. См. Stackoverflow.com/questions/379172/use-goto-or-not
Пит Киркхам
40

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

Joel Coehoorn
источник
38

Есть ли веские причины, по которым лучше иметь в функции только один оператор return?

Да , есть:

  • Единая точка выхода дает отличное место для утверждения ваших постусловий.
  • Возможность поставить точку останова отладчика на один возврат в конце функции часто бывает полезна.
  • Меньше возвратов означает меньшую сложность. Линейный код, как правило, проще для понимания.
  • Если попытка упростить функцию до единственного возврата вызывает сложность, то это является стимулом для рефакторинга в более мелкие, более общие и понятные функции.
  • Если вы говорите на языке без деструкторов или не используете RAII, то один возврат уменьшает количество мест, которые вы должны очистить.
  • Некоторые языки требуют единой точки выхода (например, Pascal и Eiffel).

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

Обновление : очевидно, руководящие принципы MISRA также предусматривают единый выход .

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

Адриан Маккарти
источник
2
Еще одна веская причина, возможно, лучшая в наши дни, - это создание единого оператора возврата. если вы хотите добавить протоколирование в метод, вы можете поместить один лог-оператор, который передает то, что возвращает метод.
INOR
Насколько распространенным было заявление FORTRAN ENTRY? См. Docs.oracle.com/cd/E19957-01/805-4939/6j4m0vn99/index.html . И если вы любите приключения, вы можете регистрировать методы с помощью AOP и последующего совета
Eric Jablow
1
+1 Первые 2 очка было достаточно, чтобы убедить меня. Это со вторым последним пунктом. Я бы не согласился с элементом логирования по той же причине, что и не одобряю глубоко вложенные условные выражения, поскольку они поощряют нарушение правила единой ответственности, которое является основной причиной, по которой полиморфизм был введен в ООП.
Фрэнсис Роджерс
Я просто хотел бы добавить, что с C # и Code Contracts проблема постусловий не возникает, так как вы все еще можете использовать Contract.Ensuresнесколько точек возврата.
Jullealgon
1
@ q126y: Если вы используете gotoдля доступа к общему коду очистки, вы, вероятно, упростили функцию, чтобы returnв конце кода очистки был один. Таким образом, можно сказать, что вы решили проблему goto, но я бы сказал, что вы решили ее, упростив до единого return.
Адриан Маккарти
33

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

отметка
источник
6
БРАВО! Вы единственный человек, который упомянул эту объективную причину. По этой причине я предпочитаю отдельные точки выхода по сравнению с несколькими точками выхода. Если бы мой отладчик мог установить точку останова на любой точке выхода, я бы предпочел несколько точек выхода. Мое текущее мнение состоит в том, что люди, которые пишут несколько точек выхода, делают это для своей выгоды за счет тех, кому приходится использовать отладчик в своем коде (и да, я говорю обо всех вас, кто пишет с открытым исходным кодом, кто пишет код с несколько точек выхода.)
MikeSchinkel
3
ДА. Я добавляю регистрационный код в систему, которая периодически неправильно работает в производственной среде (где я не могу пройти). Если бы предыдущий кодер использовал одиночный выход, это было бы НАМНОГО проще.
Майкл Блэкберн
3
Правда, в отладке это полезно. Но на практике мне удалось в большинстве случаев установить точку останова в вызывающей функции сразу после вызова - фактически, с тем же результатом. (И эта позиция, конечно, находится в стеке вызовов.) YMMV.
Foo
Это происходит только в том случае, если ваш отладчик предоставляет функцию step-out или step-return (а каждый отладчик, насколько я знаю, делает это), которая показывает возвращаемое значение сразу после возвращения. Изменение значения впоследствии может быть немного сложным, если оно не присвоено переменной, хотя.
Maarten Bodewes
7
Я давно не видел отладчика, который бы не позволял вам устанавливать точку останова на «закрытии» метода (конец, правая фигурная скобка, независимо от вашего языка) и достигать этой точки останова независимо от того, где и сколько , возвращают состояния в методе. Кроме того, даже если ваша функция имеет только один возврат, это не значит, что вы не можете выйти из функции с исключением (явным или наследуемым). Итак, я думаю, что это не совсем верный момент.
Скотт Гартнер
19

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

Скотт Дорман
источник
«В общем, я стараюсь иметь только одну точку выхода из функции» - почему? «цель должна быть как можно меньше точек выхода» - почему? И почему 19 человек проголосовали за этот ответ?
Джим Балтер
@JimBalter В конечном счете, все сводится к личным предпочтениям. Большее количество точек выхода обычно приводит к более сложному методу (хотя и не всегда) и затрудняет его понимание.
Скотт Дорман
«Это сводится к личным предпочтениям». Другими словами, вы не можете предложить причину. «Большее количество точек выхода обычно приводит к более сложному методу (хотя и не всегда)» - нет, на самом деле, нет. Учитывая две логически эквивалентные функции, одна с защитными предложениями и одна с одним выходом, последняя будет иметь более высокую цикломатическую сложность, что, как показывают многочисленные исследования, приводит к тому, что код более подвержен ошибкам и труден для понимания. Вам было бы полезно прочитать другие ответы здесь.
Джим Балтер
14

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

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

Ben Lings
источник
14

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

public void DoStuff(Foo foo)
{
    if (foo == null) return;
}

Увидев этот код, я бы сразу спросил:

  • Является ли 'foo' когда-либо нулевым?
  • Если так, сколько клиентов DoStuff когда-либо вызывали функцию с нулевым значением 'foo'?

В зависимости от ответов на эти вопросы может быть

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

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

Есть две другие причины (которые, я думаю, специфичны для кода C ++), когда множественность существует, может оказать негативное влияние. Это размер кода и оптимизация компилятора.

У не-POD C ++ объекта в области видимости при выходе из функции будет вызван деструктор. Там, где есть несколько операторов возврата, это может быть случай, когда существуют разные объекты в области видимости, поэтому список вызываемых деструкторов будет другим. Поэтому компилятор должен генерировать код для каждого оператора возврата:

void foo (int i, int j) {
  A a;
  if (i > 0) {
     B b;
     return ;   // Call dtor for 'b' followed by 'a'
  }
  if (i == j) {
     C c;
     B b;
     return ;   // Call dtor for 'b', 'c' and then 'a'
  }
  return 'a'    // Call dtor for 'a'
}

Если размер кода является проблемой, то этого стоит избегать.

Другая проблема связана с «Оптимизацией именованных возвращаемых значений» (также известной как «Копия Elision», ISO C ++ '03 12.8 / 15). C ++ позволяет реализации пропустить вызов конструктора копирования, если он может:

A foo () {
  A a1;
  // do something
  return a1;
}

void bar () {
  A a2 ( foo() );
}

Просто принимая код как есть, объект «a1» создается в «foo», и затем вызывается его конструкция копирования для создания «a2». Однако, elision copy позволяет компилятору создать «a1» в том же месте в стеке, что и «a2». Поэтому нет необходимости «копировать» объект при возврате функции.

Многочисленные точки выхода усложняют работу компилятора при попытке обнаружить это, и, по крайней мере, для сравнительно недавней версии VC ++ оптимизация не имела места, когда тело функции имело несколько возвращений. Посмотрите Оптимизацию Именованного Возвращаемого значения в Visual C ++ 2005 для получения дополнительной информации.

Ричард Корден
источник
1
Если вы возьмете все, кроме последнего dtor, из вашего примера C ++, код для уничтожения B, а затем C и B, все равно придется генерировать, когда закончится область действия оператора if, так что вы действительно ничего не получите, не имея многократных возвратов ,
Затмение
4
+1 И ваааааа внизу списка мы имеем РЕАЛЬНУЮ причину, по которой существует такая практика кодирования - NRVO. Однако это микрооптимизация; и, как и все практики микрооптимизации, вероятно, был начат примерно 50-летним «экспертом», который привык программировать на PDP-8 с частотой 300 кГц и не понимает важности чистого и структурированного кода. В общем, прислушайтесь к совету Криса С. и используйте несколько операторов return, когда это имеет смысл.
BlueRaja - Дэнни Пфлюгофт
Хотя я не согласен с вашими предпочтениями (на мой взгляд, ваше предложение Assert также является точкой возврата, как throw new ArgumentNullException()в данном случае в C #), мне очень понравились ваши другие соображения, они все действительны для меня и могут быть критическими в некоторых Нишевые контексты.
Jullealgon
Это битком набитых земляками. Вопрос о том, почему fooтестируется, не имеет ничего общего с предметом, а именно, делать ли это if (foo == NULL) return; dowork; илиif (foo != NULL) { dowork; }
Джим Балтер
11

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

Джон Ченнинг
источник
Очень проницательно Хотя я чувствую, что пока программист не знает, когда использовать несколько точек выхода, они должны быть ограничены одной.
Рик Минерих
5
На самом деле, нет. Цикломатическая сложность "if (...) return; ... return;" то же самое, что "if (...) {...} return;". У них обоих есть два пути через них.
Стив Эммерсон
11

Я заставляю себя использовать только одно returnутверждение, так как оно в некотором смысле вызывает запах кода. Позволь мне объяснить:

function isCorrect($param1, $param2, $param3) {
    $toret = false;
    if ($param1 != $param2) {
        if ($param1 == ($param3 * 2)) {
            if ($param2 == ($param3 / 3)) {
                $toret = true;
            } else {
                $error = 'Error 3';
            }
        } else {
            $error = 'Error 2';
        }
    } else {
        $error = 'Error 1';
    }
    return $toret;
}

(Условия являются произвольными ...)

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

  • Несколько возвратов
  • Рефакторинг на отдельные функции

Многократный возврат

function isCorrect($param1, $param2, $param3) {
    if ($param1 == $param2)       { $error = 'Error 1'; return false; }
    if ($param1 != ($param3 * 2)) { $error = 'Error 2'; return false; }
    if ($param2 != ($param3 / 3)) { $error = 'Error 3'; return false; }
    return true;
}

Отдельные функции

function isEqual($param1, $param2) {
    return $param1 == $param2;
}

function isDouble($param1, $param2) {
    return $param1 == ($param2 * 2);
}

function isThird($param1, $param2) {
    return $param1 == ($param2 / 3);
}

function isCorrect($param1, $param2, $param3) {
    return !isEqual($param1, $param2)
        && isDouble($param1, $param3)
        && isThird($param2, $param3);
}

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

  • создал ряд многоразовых функций,
  • сделал функцию более читабельной, и
  • фокус функций на том, почему значения являются правильными.
Jrgns
источник
5
-1: плохой пример. Вы пропустили обработку сообщения об ошибке. Если это не нужно, isCorrect можно выразить так же, как return xx && yy && zz; где xx, yy и z - выражения isEqual, isDouble и isThird.
Кауппи
10

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

Лично я никогда не слышал / не видел, чтобы "лучшие практики" говорили, что у вас должно быть только одно ответное заявление.

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

Роб Купер
источник
10

Я считаю, что множественные возвраты обычно хороши (в коде, который я пишу на C #). Стиль единственного возврата - это удержание от C. Но вы, вероятно, не программируете на C.

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

Несколько кодов возврата могут быть плохой привычкой в ​​коде C, где ресурсы должны быть явно выделены, но такие языки, как Java, C #, Python или JavaScript, имеют такие конструкции, как автоматический сбор мусора и try..finallyблоки (и usingблоки в C # ), и этот аргумент неприменим - в этих языках очень редко требуется централизованное ручное освобождение ресурсов.

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

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

Я говорил об этом более подробно в своем блоге .

Энтони
источник
10

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

Если при проверке входных данных или распределении ресурсов используются несколько точек выхода, я стараюсь очень четко поместить все «выходы с ошибками» в верхнюю часть функции.

Как в статье о спартанском программировании "SSDSLPedia", так и в статье о точках выхода из одной функции "Wiki репозитория шаблонов Portland" есть несколько проницательных аргументов. Также, конечно, есть этот пост для рассмотрения.

Если вам действительно нужна единая точка выхода (на любом языке без исключений), например, для того, чтобы высвободить ресурсы в одном месте, я считаю, что осторожное применение goto будет хорошим; посмотрите, например, этот довольно надуманный пример (сжатый, чтобы сохранить экранную недвижимость):

int f(int y) {
    int value = -1;
    void *data = NULL;

    if (y < 0)
        goto clean;

    if ((data = malloc(123)) == NULL)
        goto clean;

    /* More code */

    value = 1;
clean:
   free(data);
   return value;
}

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

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

int g(int y) {
  value = 0;

  if ((value = g0(y, value)) == -1)
    return -1;

  if ((value = g1(y, value)) == -1)
    return -1;

  return g2(y, value);
}

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

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

Короче говоря;

  • Немногие возвраты лучше многих
  • Более одного возврата лучше, чем огромные стрелы, и пункты охраны обычно в порядке.
  • Исключения могут / должны, вероятно, заменить большинство «охранных положений», когда это возможно.
Хенрик Густафссон
источник
В примере происходит сбой при y <0, потому что он пытается освободить указатель NULL ;-)
Эрих Кицмюллер
2
opengroup.org/onlinepubs/009695399/functions/free.html «Если ptr является нулевым указателем, никаких действий не происходит».
Хенрик Густафссон
1
Нет, это не приведет к сбою, потому что передача NULL в free - это определенная запретная операция Это досадно распространенное заблуждение, что сначала нужно проверить на NULL.
Хловдал
Шаблон «стрелка» не является неизбежной альтернативой. Это ложная дихотомия.
Адриан Маккарти
9

Вы знаете пословицу - красота в глазах смотрящего .

Некоторые люди клянутся NetBeans, а некоторые IntelliJ IDEA , некоторые Python, а некоторые PHP .

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

public void hello()
{
   if (....)
   {
      ....
   }
}

Все дело в наглядности и удобстве обслуживания.

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

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

Я помню, 20 лет назад мой коллега был уволен за использование того, что сегодня будет называться стратегией гибкого развития . У него был дотошный пошаговый план. Но его менеджер кричал на него: «Вы не можете постепенно предоставлять функции пользователям! Вы должны придерживаться водопада ». Он ответил менеджеру, что постепенное развитие будет более точным в соответствии с потребностями клиента. Он верил в разработку для нужд клиентов, но менеджер верил в кодирование по «требованию клиента».

Мы часто виновны в нарушении нормализации данных, границ MVP и MVC . Мы встраиваем, а не строим функцию. Мы берем ярлыки.

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

качество = точность, ремонтопригодность и рентабельность.

Все остальные правила уходят на второй план. И, конечно, это правило никогда не исчезает:

Лень - это добродетель хорошего программиста.

благословенный гик
источник
1
«Эй, приятель (как говорил бывший клиент), делай, что хочешь, если знаешь, как это исправить, когда мне нужно, чтобы ты это исправил». Проблема: часто это не ты, кто "исправляет" это.
Дэн Баррон
+1 на этот ответ, потому что я согласен с тем, куда вы идете, но не обязательно, как вы туда доберетесь. Я бы сказал, что есть уровни понимания. то есть понимание того, что сотрудник А имеет 5-летний опыт программирования и 5 лет работы в компании, сильно отличается от понимания сотрудника Б, нового выпускника колледжа, который только начинает работать в компании. Моя точка зрения заключается в том, что если сотрудник А - единственный человек, который может понять код, он НЕ подлежит сопровождению, и поэтому мы все должны стремиться написать код, понятный сотруднику В. Вот где настоящее искусство в программном обеспечении.
Фрэнсис Роджерс
9

Я склоняюсь к использованию охранных предложений, чтобы вернуться рано и в противном случае выйти в конце метода. Единственное правило входа и выхода имеет историческое значение и было особенно полезно при работе с устаревшим кодом, который занимал до 10 страниц А4 для одного метода C ++ с множественными возвратами (и многими дефектами). В последнее время общепринятой практикой является сохранение небольших методов, что делает множественные выходы менее значительными для понимания. В следующем примере Kronoz, скопированном сверху, вопрос в том, что происходит в // Остальном коде ... ?:

void string fooBar(string s, int? i) {

  if(string.IsNullOrEmpty(s) || i == null) return null;

  var res = someFunction(s, i);

  foreach(var r in res) {
      if(!r.Passed) return null;
  }

  // Rest of code...

  return ret;
}

Я понимаю, что пример несколько надуманный, но у меня возникнет соблазн реорганизовать цикл foreach в оператор LINQ, который затем можно будет рассматривать как защитное предложение. Опять же , в надуманный пример намерение кода не является очевидным и SomeFunction () может иметь какой - либо другой побочный эффект или результат может быть использован в // Остальной код ... .

if (string.IsNullOrEmpty(s) || i == null) return null;
if (someFunction(s, i).Any(r => !r.Passed)) return null;

Предоставление следующей рефакторированной функции:

void string fooBar(string s, int? i) {

  if (string.IsNullOrEmpty(s) || i == null) return null;
  if (someFunction(s, i).Any(r => !r.Passed)) return null;

  // Rest of code...

  return ret;
}
Дэвид Кларк
источник
Разве в C ++ нет исключений? Тогда зачем вам возвращаться nullвместо того, чтобы выдавать исключение, указывающее, что аргумент не принят?
Maarten Bodewes
1
Как я уже говорил, пример кода скопирован из предыдущего ответа ( stackoverflow.com/a/36729/132599 ). Исходный пример возвратил значения NULL, и рефакторинг для выдачи исключений аргументов не был существенным для того момента, который я пытался сформулировать, или для исходного вопроса. В соответствии с хорошей практикой, тогда да, я бы обычно (в C #) выбрасывал ArgumentNullException в предложении guard, а не возвращал нулевое значение.
Дэвид Кларк
7

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

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

OysterD
источник
Это на самом деле не имеет значения. Если вы измените тип возвращаемой локальной переменной, вам придется исправить все присвоения этой локальной переменной. И, вероятно, в любом случае лучше определить метод с другой сигнатурой, так как вам также придется исправить все вызовы метода.
Maarten Bodewes
@ MaartenBodewes-owlstead - это может изменить ситуацию. Чтобы назвать только два примера, в которых вам не нужно фиксировать все назначения локальной переменной или изменять вызовы метода, функция может возвращать дату в виде строки (локальной переменной будет фактическая дата, отформатированная как строка только в последний момент), или он может вернуть десятичное число, и вы хотите изменить количество десятичных знаков.
nnnnnn
@nnnnnn Хорошо, если вы хотите выполнить постобработку на выходе ... Но я бы просто сгенерировал новый метод, который выполняет постобработку, и оставил бы старый в покое. Реорганизовать его немного сложнее, но вам все равно придется проверить, совместимы ли другие вызовы с новым форматом. Но тем не менее это веская причина.
Maarten Bodewes
7

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

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

Джон Лимджап
источник
6

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

Фил Беннетт
источник
Не говоря уже о том, что вы должны сказать себе, что нужно пропустить ifоператор перед каждым вызовом метода, который возвращает успех или нет :(
Maarten Bodewes
6

Единая точка выхода - при прочих равных условиях - делает код значительно более читабельным. Но есть подвох: популярная конструкция

resulttype res;
if if if...
return res;

является подделкой, "Res =" не намного лучше, чем "возврат". Он имеет один оператор возврата, но несколько точек, где функция фактически заканчивается.

Если у вас есть функция с несколькими возвратами (или "res =" s), часто неплохо разбить ее на несколько меньших функций с одной точкой выхода.

ИМА
источник
6

Моя обычная политика - иметь только один оператор return в конце функции, если сложность кода не будет значительно уменьшена путем добавления большего количества. На самом деле, я скорее фанат Eiffel, который применяет единственное правило возврата, не имея оператора return (есть просто автоматически созданная переменная «result», в которую можно поместить ваш результат).

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

Мэтт Шеппард
источник
5

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

Perl 6: плохой пример

sub Int_to_String( Int i ){
  given( i ){
    when 0 { return "zero" }
    when 1 { return "one" }
    when 2 { return "two" }
    when 3 { return "three" }
    when 4 { return "four" }
    ...
    default { return undef }
  }
}

было бы лучше написать так

Perl 6: хороший пример

@Int_to_String = qw{
  zero
  one
  two
  three
  four
  ...
}
sub Int_to_String( Int i ){
  return undef if i < 0;
  return undef unless i < @Int_to_String.length;
  return @Int_to_String[i]
}

Обратите внимание, что это был только быстрый пример

Брэд Гилберт
источник
Хорошо, почему это было отклонено? Это не похоже на мнение.
Брэд Гилберт
5

Я голосую за Единственное возвращение в конце как руководство. Это помогает обычной обработке очистки кода ... Например, взгляните на следующий код ...

void ProcessMyFile (char *szFileName)
{
   FILE *fp = NULL;
   char *pbyBuffer = NULL:

   do {

      fp = fopen (szFileName, "r");

      if (NULL == fp) {

         break;
      }

      pbyBuffer = malloc (__SOME__SIZE___);

      if (NULL == pbyBuffer) {

         break;
      }

      /*** Do some processing with file ***/

   } while (0);

   if (pbyBuffer) {

      free (pbyBuffer);
   }

   if (fp) {

      fclose (fp);
   }
}
Alphaneo
источник
Вы голосуете за однократное возвращение - в C-коде. Но что, если вы пишете код на языке с сборкой мусора и пытаетесь ... окончательно блокировать?
Энтони
4

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

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

Эндрю Эджкомб
источник
5
Это просто другой вид преждевременной оптимизации. Вы никогда не должны оптимизировать для особого случая. Если вы обнаружите, что отлаживаете определенную часть кода много, это не так, как просто количество точек выхода.
Клин
Зависит от вашего отладчика тоже.
Maarten Bodewes
4

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

Но нет, нет ничего плохого в одном / многих операторах возврата. В некоторых языках это лучше (C ++), чем в других (C).

hazzen
источник