Есть ли веские причины, по которым лучше иметь в функции только один оператор return?
Или можно вернуться из функции, как только это будет логически правильно, то есть в функции может быть много операторов возврата?
Есть ли веские причины, по которым лучше иметь в функции только один оператор return?
Или можно вернуться из функции, как только это будет логически правильно, то есть в функции может быть много операторов возврата?
Ответы:
У меня часто есть несколько утверждений в начале метода, чтобы вернуться к «легким» ситуациям. Например, это:
public void DoStuff(Foo foo)
{
if (foo != null)
{
...
}
}
... можно сделать более читабельным (ИМХО) вот так:
public void DoStuff(Foo foo)
{
if (foo == null) return;
...
}
Так что да, я думаю, что хорошо иметь несколько «точек выхода» из функции / метода.
DoStuff() { DoStuffInner(); IncreaseStuffCallCounter(); }
Никто не упомянул или не процитировал Code Complete, поэтому я сделаю это.
Минимизируйте количество возвратов в каждой программе . Труднее понять рутину, если, читая ее внизу, вы не подозреваете о возможности ее возвращения где-то выше.
Используйте возврат, когда он улучшает читабельность . В некоторых подпрограммах, когда вы знаете ответ, вы хотите немедленно вернуть его в вызывающую подпрограмму. Если подпрограмма определена таким образом, что она не требует какой-либо очистки, то немедленный возврат не означает, что вам придется писать больше кода.
Я бы сказал, что было бы невероятно неразумно принимать произвольные решения против нескольких точек выхода, поскольку я обнаружил, что эта техника на практике оказывается полезной снова и снова , на самом деле, для ясности я часто рефакторинг существующего кода для нескольких точек выхода. Мы можем сравнить два подхода таким образом:
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;
}
Я думаю, что последнее значительно яснее. Насколько я могу судить, критика нескольких точек выхода является довольно архаичной точкой зрения в наши дни.
В настоящее время я работаю над базой кода, где двое из людей, работающих над ней, слепо подписываются на теорию «единой точки выхода», и я могу сказать вам, что по опыту это ужасная ужасная практика. Это делает код чрезвычайно сложным в обслуживании, и я покажу вам, почему.
С теорией «единой точки выхода» вы неизбежно получите код, который выглядит следующим образом:
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 и фигурные скобки соответствуют друг другу.
Этот метод делает обслуживание кода чрезвычайно сложным и подверженным ошибкам.
Структурированное программирование говорит, что вы должны иметь только один оператор возврата для каждой функции. Это для ограничения сложности. Многие люди, такие как Мартин Фаулер, утверждают, что проще писать функции с несколькими операторами возврата. Он представляет этот аргумент в классической книге рефакторинга, которую он написал. Это хорошо работает, если вы будете следовать его другим советам и писать небольшие функции. Я согласен с этой точкой зрения, и только пуристы строгого структурированного программирования придерживаются единственного оператора return для каждой функции.
GOTO
для перемещения потока управления, даже когда функция существовала. Он никогда не говорит "никогда не использовать GOTO
".
Как отмечает Кент Бек, обсуждая защитные предложения в шаблонах реализации, у подпрограммы есть одна точка входа и выхода ...
«Предотвращение путаницы, возможной при переходе во многие места в одной и той же подпрограмме. Это имело смысл применительно к программам на языке FORTRAN или ассемблере, написанным с большим количеством глобальных данных, где даже понимание того, какие операторы были выполнены, было тяжелой работой ... . с небольшими методами и в основном локальными данными, это излишне консервативно ".
Я нахожу функцию, написанную с защитными предложениями, намного более легкой, чем одна длинная вложенная группа if then else
утверждений.
В функции, которая не имеет побочных эффектов, нет веской причины иметь более одного возврата, и вы должны написать их в функциональном стиле. В методе с побочными эффектами вещи более последовательны (индексируются по времени), поэтому вы пишете в императивном стиле, используя инструкцию return в качестве команды для прекращения выполнения.
Другими словами, по возможности, поддерживайте этот стиль
return a > 0 ?
positively(a):
negatively(a);
через это
if (a > 0)
return positively(a);
else
return negatively(a);
Если вы обнаружите, что пишете несколько слоев вложенных условий, возможно, есть способ реорганизовать это, например, используя список предикатов. Если вы обнаружите, что ваши if и elses синтаксически далеко друг от друга, вы можете разбить это на более мелкие функции. Условный блок, который занимает больше, чем экранный текст, трудно читать.
Там нет жесткого и быстрого правила, которое применяется к каждому языку. Что-то вроде наличия одного оператора return не сделает ваш код хорошим. Но хороший код позволит вам писать свои функции таким образом.
Я видел это в стандартах кодирования для C ++, которые были пережитком C, как если бы у вас не было RAII или другого автоматического управления памятью, тогда вы должны очищать для каждого возврата, что либо означает вырезать и вставить очистки или перехода (логически то же самое, что «наконец» в управляемых языках), оба из которых считаются дурным тоном. Если вы практикуете использование умных указателей и коллекций в C ++ или другой автоматической системе памяти, то для этого нет веских оснований, и все сводится к удобочитаемости и, скорее, к суждению.
auto_ptr
, вы можете использовать простые указатели параллельно. Хотя было бы странно писать «оптимизированный» код с неоптимизирующим компилятором.
try
... finally
в Java), и вам нужно выполнить обслуживание ресурсов, которое вы могли бы сделать с одним возврат в конце метода. Прежде чем сделать это, вы должны серьезно подумать о рефакторинге кода, чтобы избавиться от ситуации.
Я склоняюсь к мысли, что операторы return в середине функции плохие. Вы можете использовать return для создания нескольких защитных предложений в верхней части функции и, конечно, сообщить компилятору, что возвращать в конце функции без проблем, но возврат в середине функции может быть легко пропущен и может усложнить интерпретацию функции.
Есть ли веские причины, по которым лучше иметь в функции только один оператор return?
Да , есть:
Вопрос часто ставится как ложная дихотомия между множественными возвратами или глубоко вложенными операторами if. Почти всегда существует третье решение, которое является очень линейным (без глубокого вложения) с единственной точкой выхода.
Обновление : очевидно, руководящие принципы MISRA также предусматривают единый выход .
Чтобы быть ясным, я не говорю, что это всегда неправильно иметь несколько возвратов. Но, учитывая иные эквивалентные решения, есть много веских причин, чтобы предпочесть тот, с одним возвратом.
Contract.Ensures
несколько точек возврата.
goto
для доступа к общему коду очистки, вы, вероятно, упростили функцию, чтобы return
в конце кода очистки был один. Таким образом, можно сказать, что вы решили проблему goto
, но я бы сказал, что вы решили ее, упростив до единого return
.
Наличие единственной точки выхода действительно дает преимущество в отладке, поскольку позволяет установить единственную точку останова в конце функции, чтобы увидеть, какое значение на самом деле будет возвращено.
В общем, я стараюсь иметь только одну точку выхода из функции. Однако бывают случаи, когда это фактически приводит к созданию более сложного тела функции, чем необходимо, и в этом случае лучше иметь несколько точек выхода. Это действительно должен быть «призыв к суждению», основанный на результирующей сложности, но цель должна состоять в том, чтобы как можно меньше точек выхода без ущерба для сложности и понятности.
Нет, потому что мы больше не живем в 1970-х годах . Если ваша функция достаточно длинная, чтобы множественные возвраты были проблемой, она слишком длинная.
(Совершенно независимо от того факта, что любая многострочная функция в языке с исключениями в любом случае будет иметь несколько точек выхода.)
Я бы предпочел единый выход, если он действительно не усложнит ситуацию. Я обнаружил, что в некоторых случаях несколько существующих точек могут маскировать другие более важные проблемы проектирования:
public void DoStuff(Foo foo)
{
if (foo == null) return;
}
Увидев этот код, я бы сразу спросил:
В зависимости от ответов на эти вопросы может быть
В обоих вышеупомянутых случаях код, вероятно, может быть переработан с утверждением, гарантирующим, что '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 для получения дополнительной информации.
throw new ArgumentNullException()
в данном случае в C #), мне очень понравились ваши другие соображения, они все действительны для меня и могут быть критическими в некоторых Нишевые контексты.
foo
тестируется, не имеет ничего общего с предметом, а именно, делать ли это if (foo == NULL) return; dowork;
илиif (foo != NULL) { dowork; }
Наличие единой точки выхода уменьшает цикломатическую сложность и, следовательно, теоретически снижает вероятность того, что вы будете вносить ошибки в свой код при его изменении. Однако практика, как правило, предполагает, что необходим более прагматичный подход. Поэтому я стремлюсь иметь одну точку выхода, но позволяю моему коду иметь несколько, если это более читабельно.
Я заставляю себя использовать только одно 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);
}
Конечно, это длиннее и немного грязно, но в процессе рефакторинга функции таким образом, мы
Я бы сказал, что у вас должно быть столько, сколько требуется, или любое, что делает код чище (например, пункты охраны ).
Лично я никогда не слышал / не видел, чтобы "лучшие практики" говорили, что у вас должно быть только одно ответное заявление.
По большей части я стремлюсь как можно скорее выйти из функции, основываясь на логическом пути (пункты охраны - отличный пример этого).
Я считаю, что множественные возвраты обычно хороши (в коде, который я пишу на C #). Стиль единственного возврата - это удержание от C. Но вы, вероятно, не программируете на C.
Нет закона, требующего только одну точку выхода для метода на всех языках программирования . Некоторые люди настаивают на превосходстве этого стиля, и иногда они возводят его в «правило» или «закон», но это убеждение не подтверждается никакими доказательствами или исследованиями.
Несколько кодов возврата могут быть плохой привычкой в коде C, где ресурсы должны быть явно выделены, но такие языки, как Java, C #, Python или JavaScript, имеют такие конструкции, как автоматический сбор мусора и try..finally
блоки (и using
блоки в C # ), и этот аргумент неприменим - в этих языках очень редко требуется централизованное ручное освобождение ресурсов.
Есть случаи, когда одно возвращение является более читабельным, и случаи, когда это не так. Посмотрите, уменьшает ли это количество строк кода, делает логику более понятной или уменьшает количество фигурных скобок и отступов или временных переменных.
Поэтому используйте столько возвратов, сколько соответствует вашим художественным способностям, потому что это вопрос макета и читаемости, а не технический.
Я говорил об этом более подробно в своем блоге .
Есть хорошие вещи, которые нужно сказать о наличии единой точки выхода, так же как и плохие вещи, которые нужно сказать о неизбежном программировании «стрелок», которое в результате получается.
Если при проверке входных данных или распределении ресурсов используются несколько точек выхода, я стараюсь очень четко поместить все «выходы с ошибками» в верхнюю часть функции.
Как в статье о спартанском программировании "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 (или вообще, если бы мне это удавалось без особых усилий), и код был бы намного более простым. И без стрелок. И большинство не окончательных результатов, вероятно, будут исключениями.
Короче говоря;
Вы знаете пословицу - красота в глазах смотрящего .
Некоторые люди клянутся NetBeans, а некоторые IntelliJ IDEA , некоторые Python, а некоторые PHP .
В некоторых магазинах вы можете потерять работу, если будете настаивать на этом:
public void hello()
{
if (....)
{
....
}
}
Все дело в наглядности и удобстве обслуживания.
Я пристрастился к использованию булевой алгебры для сокращения и упрощения логики и использования конечных автоматов. Однако, в прошлом были коллеги, которые полагали, что мое использование «математических методов» в кодировании не подходит, потому что оно не будет видимым и поддерживаемым. И это было бы плохой практикой. Извините, люди, которые я использую, для меня очень наглядны и понятны, потому что, когда я вернусь к коду шесть месяцев спустя, я ясно пойму код, а не беспорядочные спагетти.
Эй, приятель (как обычно говорил бывший клиент), делай что хочешь, если знаешь, как это исправить, когда мне нужно, чтобы ты это исправил.
Я помню, 20 лет назад мой коллега был уволен за использование того, что сегодня будет называться стратегией гибкого развития . У него был дотошный пошаговый план. Но его менеджер кричал на него: «Вы не можете постепенно предоставлять функции пользователям! Вы должны придерживаться водопада ». Он ответил менеджеру, что постепенное развитие будет более точным в соответствии с потребностями клиента. Он верил в разработку для нужд клиентов, но менеджер верил в кодирование по «требованию клиента».
Мы часто виновны в нарушении нормализации данных, границ MVP и MVC . Мы встраиваем, а не строим функцию. Мы берем ярлыки.
Лично я считаю, что PHP - плохая практика, но что я знаю. Все теоретические аргументы сводятся к попытке выполнить один набор правил
качество = точность, ремонтопригодность и рентабельность.
Все остальные правила уходят на второй план. И, конечно, это правило никогда не исчезает:
Лень - это добродетель хорошего программиста.
Я склоняюсь к использованию охранных предложений, чтобы вернуться рано и в противном случае выйти в конце метода. Единственное правило входа и выхода имеет историческое значение и было особенно полезно при работе с устаревшим кодом, который занимал до 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;
}
null
вместо того, чтобы выдавать исключение, указывающее, что аргумент не принят?
Одна хорошая причина, которую я могу придумать, заключается в обслуживании кода: у вас есть единственная точка выхода. Если вы хотите изменить формат результата, ... это гораздо проще реализовать. Также для отладки вы можете просто поставить точку останова там :)
Сказав это, мне когда-то приходилось работать в библиотеке, где стандарты кодирования накладывали «один оператор возврата на функцию», и я нашел это довольно жестким. Я пишу много кода для числовых вычислений, и часто бывают «особые случаи», так что в конечном итоге за кодом было довольно трудно следовать ...
Несколько точек выхода подходят для достаточно небольших функций, то есть функции, которую можно просматривать на одном экране в целом. Если длинная функция также включает в себя несколько точек выхода, это признак того, что эту функцию можно продолжить.
Тем не менее, я избегаю функций множественного выхода, если это абсолютно необходимо . Я чувствовал боль от ошибок, которые происходят из-за некоторого случайного возврата в какой-то непонятной строке в более сложных функциях.
Я работал с ужасными стандартами кодирования, которые навязывали вам единый путь выхода, и результатом почти всегда были неструктурированные спагетти, если функция не тривиальна - вы в конечном итоге получаете множество разрывов и продолжений, которые просто мешают.
if
оператор перед каждым вызовом метода, который возвращает успех или нет :(
Единая точка выхода - при прочих равных условиях - делает код значительно более читабельным. Но есть подвох: популярная конструкция
resulttype res;
if if if...
return res;
является подделкой, "Res =" не намного лучше, чем "возврат". Он имеет один оператор возврата, но несколько точек, где функция фактически заканчивается.
Если у вас есть функция с несколькими возвратами (или "res =" s), часто неплохо разбить ее на несколько меньших функций с одной точкой выхода.
Моя обычная политика - иметь только один оператор return в конце функции, если сложность кода не будет значительно уменьшена путем добавления большего количества. На самом деле, я скорее фанат Eiffel, который применяет единственное правило возврата, не имея оператора return (есть просто автоматически созданная переменная «result», в которую можно поместить ваш результат).
Конечно, есть случаи, когда код можно сделать более понятным с множественными возвратами, чем очевидная версия без них. Можно утверждать, что требуется дополнительная доработка, если у вас есть функция, которая слишком сложна, чтобы ее можно было понять без множественных операторов возврата, но иногда полезно быть прагматичным в таких вещах.
Если вы получите более нескольких возвратов, возможно, что-то не так с вашим кодом. В противном случае я бы согласился, что иногда приятно иметь возможность возвращаться из нескольких мест в подпрограмме, особенно когда это делает код чище.
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 }
}
}
было бы лучше написать так
@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]
}
Обратите внимание, что это был только быстрый пример
Я голосую за Единственное возвращение в конце как руководство. Это помогает обычной обработке очистки кода ... Например, взгляните на следующий код ...
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);
}
}
Вероятно, это необычная перспектива, но я думаю, что любому, кто считает, что нужно использовать несколько операторов return, никогда не приходилось использовать отладчик на микропроцессоре, который поддерживает только 4 аппаратных точки останова. ;-)
Хотя проблемы с «кодом стрелки» полностью верны, одна проблема, которая, похоже, исчезает при использовании нескольких операторов return, - это ситуация, когда вы используете отладчик. У вас нет удобной универсальной позиции для установки точки останова, чтобы гарантировать, что вы увидите выход и, следовательно, условие возврата.
Чем больше операторов возврата у вас есть в функции, тем выше сложность в этом одном методе. Если вам интересно, есть ли у вас слишком много возвращаемых операторов, вы можете спросить себя, не слишком ли много строк кода в этой функции.
Но нет, нет ничего плохого в одном / многих операторах возврата. В некоторых языках это лучше (C ++), чем в других (C).