一个函数应该只有一个return语句吗?


780

是否有充分的理由说明为什么在函数中仅包含一个return语句是一种更好的做法?

还是在逻辑上正确地从函数中返回就可以,这意味着函数中可能有很多return语句?


25
我不同意这个问题与语言无关。对于某些语言,与其他语言相比,拥有多种回报更为自然和便捷。与使用RAII的C ++相比,我更可能抱怨C函数的早期返回。
Adrian McCarthy

3
这是密切相关,而且具有优良的答案:programmers.stackexchange.com/questions/118703/...
蒂姆Schmelter

语言不可知的?向使用功能语言的人解释,他必须为每个功能使用一个返回值:p
Boiethios,

Answers:


741

在方法开始时,我经常会有几条陈述返回“轻松”情况。例如,这:

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

...可以像这样变得更具可读性(IMHO):

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

    ...
}

因此,是的,我认为从一个函数/方法中获得多个“退出点”很好。


83
同意 尽管拥有多个退出点可能会失控,但我绝对认为这比将整个函数放在IF块中更好。尽可能合理地使用return来保持代码的可读性。
Joshua Carmody

172
这就是所谓的“保护声明”,是福勒的重构。
拉尔斯·韦斯特格伦

12
当函数保持相对较短时,不难在返回点的中间跟随函数的结构。
KJAWolf,2009年

21
巨大的if-else语句块,每个语句都有一个返回值?没什么 这样的事情通常很容易重构。(至少具有结果变量的单出口变体是最常见的,所以我怀疑多出口变体会更困难。)如果您真的很头疼,请查看if-else和while循环的组合(由本地控制)布尔值),其中设置了结果变量,导致在方法末尾的最终退出点。那是单一出口想法发疯了,是的,我是从不得不处理的实际经验出发。
MarcusAndrén'09年

7
'想象:您需要在'DoStuff'函数末尾执行“ IncreaseStuffCallCounter”方法。在这种情况下,您会怎么做?:)'-– DoStuff() { DoStuffInner(); IncreaseStuffCallCounter(); }
吉姆·巴尔特

355

没有人提及或引用“代码完成”,所以我会做。

17.1返回

最小化每个例程中的返回数。如果在底部阅读该例程,而又没有意识到该例程可能会返回到上方,则很难理解该例程。

在提高可读性时使用return。在某些例程中,一旦知道答案,就想立即将其返回给调用例程。如果例程的定义不需要任何清理,则不立即返回就意味着您必须编写更多代码。


64
为“最小化”的细微差别+1,但不禁止多次回报。
拉德瓦尔德

13
“难以理解”是非常主观的,尤其是当作者没有提供任何经验证据来支持一般性主张时……对于必须在最终返回语句的代码中的许多条件位置设置单个变量的情况同样如此“没有意识到将变量分配给函数上方某个位置的可能性!
Heston T. Holtmann

26
就个人而言,我希望尽可能早日返回。为什么?好吧,当您看到给定案例的return关键字时,您会立即知道“我已经完成”了-您不必继续阅读就可以确定之后会发生什么。
马克·辛普森

12
@ HestonT.Holtmann:那所做的事情代码完成编程书籍中是独一无二的是,该建议由经验证据的支持。
阿德里安·麦卡锡

9
这可能应该是公认的答案,因为它提到拥有多个返回点并不总是一件好事,但有时却是必要的。
拉菲德2014年

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

我认为后者要清楚得多。据我所知,如今对多个出口点的批评是一种过时的观点。


12
旁观者眼中的清晰度-我着眼于一种功能,寻找起点和终点。当函数很小时,它很好-但是,当您尝试弄清为什么某些东西坏了并且“ Rest of Code”变得不平凡时,您可以花一些时间寻找ret的原因
Murph

7
首先,这是人为的例子。第二,:string ret;“在第二个版本中放哪里?第三,Ret不包含有用的信息。第四,为什么一个函数/方法中包含如此多的逻辑?第五,为什么不将DidValuesPass(type res)然后RestOfCode()分开子功能?
瑞克·麦里奇

25
@Rick 1.经验不足,这实际上是我遇到过的多次模式,2.分配在“其余代码”中,也许还不清楚。3.嗯?这是一个例子吗?4.好吧,我想这是做作的,但是有可能对此做充分的说明,5.可以做...
ljs

5
@Rick的重点是,提早返回通常比将代码包装在一个巨大的if语句中要容易得多。即使有适当的重构,根据我的经验,它也会发挥很多作用。
ljs

5
没有多个return语句的要点是使调试更容易,其次是可读性。最后的一个断点允许您查看出口值,没有例外。至于可读性,所示的两个功能的行为并不相同。如果!r.Passed,则默认返回为空字符串,但“更易读”的字符串将其更改为返回null。作者误读了前面几行后才有默认值。即使在琐碎的示例中,也很容易获得不清楚的默认返回值,而最后的单个返回值有助于强制执行。
米奇

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之间添加一个操作。您必须缩进整个freaking函数,并祝您好运,确保所有您的if / else条件和括号正确匹配。

这种方法使代码维护极为困难且容易出错。


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 =“语句都在出口路径上。这不仅仅是退出函数,还涉及退出任何块或序列。
Jay Bazuzi

6
我不同意单个返回“不可避免地”会导致深度嵌套。您的示例可以写成一个简单的,线性的函数,只有一个返回值(没有gotos)。而且,如果您不或者不能完全依靠RAII进行资源管理,那么早期的回报最终会导致泄漏或重复代码。最重要的是,提早归还使主张后置条件变得不切实际。
阿德里安·麦卡锡

72

结构化编程说每个函数只能有一个return语句。这是为了限制复杂性。许多人(例如Martin Fowler)认为,使用多个return语句编写函数更简单。他在他撰写的经典重构书中提出了这一论点。如果您遵循他的其他建议并编写了一些小函数,则此方法效果很好。我同意这种观点,并且只有严格的结构化编程纯粹主义者遵守每个函数的单个返回语句。


44
结构化编程没有说什么。一些(但不是全部)自称为结构化程序设计倡导者的人说。
08年

15
“如果您遵循他的其他建议并编写小的函数,那么这将非常有效。” 这是最重要的一点。小型函数很少需要许多返回点。

6
@wnoise +1表示评论,非常正确。所有“结构化编程”都说不要使用GOTO。
paxos1977

1
@ceretullis:除非必要。当然,它不是必需的,但在C语言中可能有用。Linux内核使用它是有充分理由的。GOTO认为Harmful谈到GOTO即使在功能存在时也使用移动控制流。它从不说“从不使用GOTO”。
EstebanKüber09年

1
“都是关于忽略代码的“结构”。”-不,恰恰相反。“说应该避免使用它们是有道理的”-不,不是。
Jim Balter'1

62

正如肯特·贝克(Kent Beck)在讨论实现模式中的保护条款时所指出的,使例程具有单个入口和出口点...

“这是为了防止在同一例程中跳入和跳出许多位置时可能造成的混乱。将其应用于用大量全局数据编写的FORTRAN或汇编语言程序时,即使了解执行了哪些语句也很困难,这是很有意义的。 ……使用小的方法和大部分本地数据,就显得过于保守了。”

我发现用一个保护子句编写的函数比一堆长嵌套的if then else语句容易遵循。


当然,“一堆if-then-else长嵌套嵌套语句”不是保护子句的唯一选择。
Adrian McCarthy 2014年

@AdrianMcCarthy您有更好的选择吗?这比讽刺更有用。
shinzou

@kuhaku:我不确定我会叫那个讽刺。答案表明这是一种“或/或”情形:保护子句或if-then-else的长串嵌套。除了保护子句外,许多(大多数?)编程语言还提供了许多方法来分解这种逻辑。
阿德里安·麦卡锡

61

在没有副作用的函数中,没有充分的理由要有一个以上的收益,而应该以函数样式编写它们。在具有副作用的方法中,事情更具顺序性(按时间索引),因此您以命令式的方式编写,将return语句用作停止执行的命令。

换句话说,请尽可能支持这种样式

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

在这个

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

如果您发现自己编写了多层嵌套条件,则可能有一种方法可以重构它,例如使用谓词列表。如果您发现if和else在语法上相距甚远,则可能需要将其分解为较小的函数。跨越多个屏幕文本的条件块很难阅读。

没有适用于每种语言的严格规则。诸如拥有单个return语句之类的东西不会使您的代码良好。但是好的代码将倾向于允许您以这种方式编写函数。


6
+1“如果发现if和else在语法上相距甚远,则可能需要将其分解为较小的函数。”
Andres Jaan Tack

4
+1,如果这是一个问题,通常意味着您在单个函数中做了太多事情。这真的让我感到沮丧,这不是投票率最高的答案
马特·布里格斯

1
Guard语句也没有任何副作用,但是大多数人会认为它们很有用。因此,即使没有副作用,也可能有理由尽早停止执行。我认为此答案无法完全解决问题。
Maarten Bodewes,2015年

@ MaartenBodewes-owlstead请参见“严和懒惰”
Apocalisp

43

我已经在C ++的编码标准中看到了这一点,它是C的遗留物,就好像您没有RAII或其他自动内存管理一样,因此您必须为每次返回清理一下,这意味着剪切和粘贴清理或转到(逻辑上与托管语言中的“最终”相同),这两种形式均被视为错误形式。如果您的做法是在C ++或其他自动内存系统中使用智能指针和集合,则没有充分的理由,这全都与可读性有关,更多地是判断力。


说得好,尽管我确实相信在尝试编写高度优化的代码(例如对复杂的3d网格进行软件蒙皮!)时,最好复制删除内容
Grant Peters 2009年

1
是什么让您相信这一点?如果您的编译器优化不佳,则在取消引用时会有一些开销,则auto_ptr可以并行使用普通指针。尽管首先使用非优化编译器编写“优化”代码是很奇怪的。
皮特·柯坎

这使该规则成为一个有趣的例外:如果您的编程语言不包含在方法末尾自动调用的内容(例如Java中的try... finally),并且您需要执行资源维护,则只需一个即可在方法末尾返回。在执行此操作之前,您应该认真考虑重构代码以摆脱这种情况。
Maarten Bodewes 2011年

@PeteKirkham为什么goto清理不好?是的,goto可能使用不佳,但是这种特殊用法也不错。
q126y

1
与RAII不同,C ++中的@ q126y在引发异常时失败。在C语言中,这是一种完全有效的做法。参见stackoverflow.com/questions/379172/use-goto-or-not
Pete Kirkham

40

我倾向于认为函数中间的return语句是不好的。您可以使用return在函数顶部构建一些保护子句,并且当然告诉编译器在函数末尾没有问题的情况下返回什么,但是在中间返回函数的可能很容易遗漏并且可以使函数难以解释。


38

是否有充分的理由说明为什么在函数中仅包含一个return语句是一种更好的做法?

,有:

  • 单个出口点是声明后置条件的绝佳场所。
  • 能够在函数末尾的一个返回上放置调试器断点通常很有用。
  • 更少的回报意味着更少的复杂性。线性代码通常更容易理解。
  • 如果试图将一个函数简化为单一收益会导致复杂性,那么就有动力重构为更小,更通用,更易于理解的函数。
  • 如果您使用的语言没有析构函数,或者您不使用RAII,则单次返回可以减少需要清理的地方数量。
  • 一些语言需要一个出口点(例如,Pascal和Eiffel)。

这个问题经常被认为是多次返回之间的错误二分法,或者是嵌套在if语句中的嵌套。几乎总是有第三个解决方案,它只有一个出口点就非常线性(没有深层嵌套)。

更新:显然,MISRA准则也促进了单一出口

明确地说,我并不是说拥有多个回报总是错误的。但是,如果给出了其他等效的解决方案,则有很多充分的理由偏爱单收益的解决方案。


2
另一个很好的理由(可能是最近最好的)是拥有一个return语句。如果要将日志记录添加到方法,则可以放置单个log语句来传达方法返回的内容。
2013年

FORTRAN ENTRY语句有多普遍?请参阅docs.oracle.com/cd/E19957-01/805-4939/6j4m0vn99/index.html。如果您喜欢冒险,可以使用AOP和
后续

1
+1前2分足以说服我。倒数第二段。我不同意日志记录元素,原因与我不鼓励深度嵌套的条件语句一样,因为它们鼓励打破单一责任规则,这是将多态性引入OOP的主要原因。
弗朗西斯·罗杰斯2014年

我只想补充一点,对于C#和代码合同,后置条件问题不是问题,因为您仍然可以使用Contract.Ensures多个返回点。
julealgon 2014年

1
@ q126y:如果您习惯使用goto常见的清理代码,则可能已经简化了该函数,因此清理代码return的末尾只有一个。因此,您可以说您已经解决了问题goto,但我想通过简化为一个解决了问题return
阿德里安·麦卡锡

33

具有单个出口点确实在调试中提供了一个优势,因为它使您可以在函数的末尾设置单个断点,以查看实际将返回的值。


6
太棒了!您是唯一提及此客观原因的人。这就是我偏爱单个出口点而不是多个出口点的原因。如果我的调试器可以在任何出口点上设置断点,则我可能更希望使用多个出口点。我当前的观点是,为多个出口点编写代码的人是为了自己的利益而这样做,而这是在其他必须在代码上使用调试器的人的代价下进行的(是的,我正在谈论所有使用这些代码编写代码的开源贡献者。多个出口。)
MikeSchinkel 2010年

3
是。我正在将日志记录代码添加到在生产中间歇出现异常的系统(无法逐步执行)。如果以前的编码器使用过一次出口,那将更加容易。
迈克尔·布莱克本

3
是的,在调试中很有用。但是实际上,在大多数情况下,在调用之后,我就能在调用函数中设置断点-有效地获得相同的结果。(当然,该位置可以在调用堆栈中找到。)YMMV。
foo

除非调试器提供了逐步或逐步返回功能(据我所知,每个调试器都执行此功能),否则该函数会在返回后立即显示返回值。如果未将值赋给变量,则事后更改值可能会有些棘手。
Maarten Bodewes 2014年

7
我已经很长时间没有看到调试器了,这不允许您在方法的“关闭”位置设置一个断点(结束,右花括号,无论您使用哪种语言),无论在何处或多少处都击中该断点,返回statemets在方法中。同样,即使您的函数只有一个返回值,这也不意味着您无法退出带有异常(显式或继承)的函数。因此,我认为这并不是一个正确的观点。
Scott Gartner

19

通常,我尝试从功能中仅获得一个出口。但是,有时这样做实际上最终会创建一个比必要的复杂的函数体,在这种情况下,最好有多个出口点。实际上,这必须是基于结果复杂性的“判断调用”,但是目标应该是在不牺牲复杂性和可理解性的情况下尽可能减少出口。


“总的来说,我尝试从一个功能中仅拥有一个出口点”-为什么?“目标应该是尽可能减少出口点”-为什么?为什么有19个人对这个非答案投票?
Jim Balter'1

@JimBalter最终,归结为个人喜好。更多的出口点通常会导致更复杂的方法(尽管并非总是如此),并使人们更难以理解。
Scott Dorman

“这归结为个人喜好。”-换句话说,您无法提供理由。“更多的出口点通常会导致更复杂的方法(尽管并非总是如此)”-不,实际上,事实并非如此。给定两个在逻辑上等效的函数,一个具有保护子句,一个具有单个出口,后者将具有更高的循环复杂性,大量研究表明,代码中的结果更容易出错并且难以理解。您将从此处阅读其他答复中受益。
Jim Balter'1

14

不,因为我们不再生活在1970年代。如果您的函数足够长而导致多次返回是一个问题,那就太长了。

(除了以下事实外,任何语言中的任何多行函数(带有例外)都将有多个出口点。)


14

我的选择是单次退出,除非它确实使事情复杂化。我发现在某些情况下,多个存在点可以掩盖其他更重要的设计问题:

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

看到此代码后,我将立即询问:

  • “ foo”曾经为空吗?
  • 如果是这样,'DoStuff'的多少个客户曾经使用空'foo'调用该函数?

根据这些问题的答案,可能是

  1. 该检查毫无意义,因为它永远都不是真实的(即它应该是一个断言)
  2. 该检查很少是真的,因此最好更改那些特定的调用程序函数,因为它们可能仍应采取其他措施。

在以上两种情况下,都可以使用断言重新编写代码,以确保'foo'永远不会为null且相关的调用者已更改。

还有两个其他原因(我认为是C ++代码特定的原因),多个存在实际上可能产生负面影响。它们是代码大小和编译器优化。

作用域中位于函数出口处的非POD C ++对象将调用其析构函数。在有多个return语句的情况下,范围可能是不同的对象,因此要调用的析构函数的列表将有所不同。因此,编译器需要为每个return语句生成代码:

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

如果代码大小是一个问题-那么这可能值得避免。

另一个问题与“命名返回值优化”有关(又名Copy Elision,ISO C ++ '03 12.8 / 15)。C ++允许实现在可能的情况下跳过对复制构造函数的调用:

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

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

仅按原样执行代码,就在“ foo”中构造对象“ a1”,然后将调用其副本构造来构造“ a2”。但是,复制省略允许编译器在堆栈上与“ a2”相同的位置构造“ a1”。因此,函数返回时无需“复制”对象。

多个出口点使编译器试图检测到这一点变得很复杂,至少对于最新版本的VC ++,在函数体具有多个返回的情况下并没有进行优化。有关更多详细信息,请参见Visual C ++ 2005中的命名返回值优化


1
如果您将C ++示例中除最后一个dtor之外的所有内容都带走,那么在if语句的作用域结束时,仍然必须生成销毁B以及后来的C和B的代码,因此,由于没有多次返回,您实际上一无所获。

4
+1并且在列表的底部,我们确实有这种编码实践存在的真正原因 -NRVO。但是,这是微优化。就像所有微优化实践一样,它可能是由大约50岁的“专家”开始的,该专家习惯于在300 kHz PDP-8上进行编程,并且不了解干净和结构化代码的重要性。通常,请听取Chris S的建议,并在需要时使用多个return语句。
BlueRaja-Danny Pflughoeft,2010年

尽管我不同意您的偏爱(在我看来,您的Assert建议也是一个返回点,就像throw new ArgumentNullException()本例中的C#一样),但我真的很喜欢您的其他注意事项,它们对我都是有效的,并且在某些情况下可能很关键利基环境。
julealgon 2014年

这挤满了稻草人。为什么这个问题foo正在测试无关与主题,这是无论做if (foo == NULL) return; dowork; if (foo != NULL) { dowork; }
吉姆·巴尔特

11

具有单个出口点可降低循环复杂性,因此从理论上讲,可以减少更改代码时将错误引入代码中的可能性。然而,实践往往表明需要一种更务实的方法。因此,我倾向于将目标指向一个出口,但如果可读性更高,则允许我的代码具有多个出口。


非常有见地。虽然,我觉得在程序员不知道何时使用多个出口点之前,应该将它们限制为一个。
Rick Minerich,2009年

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

当然,它更长并且有点混乱,但是在以这种方式重构函数的过程中,我们已经

  • 创建了许多可重用的功能,
  • 使该功能更易于阅读,并且
  • 函数的重点在于为什么值正确。

5
-1:不好的例子。您已经省略了错误消息处理。如果不需要,则可以将isCorrect表示为return xx && yy && zz;。其中xx,yy和z是isEqual,isDouble和isThird表达式。
kauppi

10

我要说的是,您应该拥有所需的数量,或者可以使代码更整洁的数量(例如guard子句)。

我个人从未听说过/见过任何“最佳做法”,即您只应有一份退货声明。

在大多数情况下,我倾向于根据逻辑路径尽快退出函数(保护子句就是一个很好的例子)。


10

我相信多次返回通常是好的(在我用C#编写的代码中)。单返回样式是C的保留。但是您可能没有使用C进行编码。

在所有编程语言中,没有法律只要求一个方法的退出点。有些人坚持这种风格的优越性,有时他们将其提升为“规则”或“法律”,但是这种信念没有任何证据或研究的支持。

不止一种返回样式在C代码中是一个坏习惯,在C代码中必须显式地取消分配资源,但是Java,C#,Python或JavaScript之类的语言具有自动垃圾收集和try..finally块(以及usingC#中的块)构造),并且该参数不适用-在这些语言中,需要集中手动分配资源非常罕见。

在某些情况下,单项退货更具可读性,而在某些情况下则不易理解。看看它是否减少了代码行数,使逻辑更清晰或减少了花括号,缩进或临时变量的数量。

因此,使用尽可能多的退货来满足您的艺术敏感性,因为这是布局和可读性问题,而不是技术问题。

我已经在博客上更详细地讨论了这一点


10

关于具有单个出口点,有很多好话要说,就像对不可避免的“箭头”编程有不好的话要说。

如果在输入验证或资源分配过程中使用多个出口点,我会尝试将所有“错误出口”放在功能顶部。

关于“ SSDSLPedia” 的Spartan Programming文章和“ Portland Pattern Repository的Wiki”的“ 单功能出口点”文章都对此有一些有见地的争论。另外,当然,有这篇文章要考虑。

例如,如果您确实想要一个出口点(使用任何启用了非异常功能的语言)以便在一个地方释放资源,那么我发现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非常易于单独测试。

显然,在面向对象和启用了异常的语言中,我不会使用这样的if语句(或者根本不会使用足够少的精力就可以摆脱它),并且代码会更简单。和非箭头。而且大多数非最终回报可能都是例外。

简而言之;

  • 很少有回报比许多回报要好
  • 一个以上的回报比巨大的箭头要好,而且保护子句通常还可以。
  • 在可能的情况下,异常可以/应该替换大多数“保护条款”。

该示例因y <0而崩溃,因为它尝试释放NULL指针;-)
Erich Kitzmueller 09年

2
opengroup.org/onlinepubs/009695399/functions/free.html “如果ptr是空指针,则不会发生任何动作。”
亨里克·古斯塔夫森

1
不,它不会崩溃,因为将NULL传递给free是已定义的no-op。您必须首先测试NULL是一种令人讨厌的普遍误解。
hlovdal

“箭头”模式不是不可避免的选择。这是错误的二分法。
Adrian McCarthy

9

你知道格言- 情人在旁观者眼中

有些人发誓的NetBeans和一些由IntelliJ IDEA的,一些由Python的一些由PHP

如果坚持这样做,在某些商店中,您可能会失业:

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

问题全在于可见性和可维护性。

我沉迷于使用布尔代数来简化和简化逻辑以及使用状态机。但是,以前的同事们认为我在编码中使用“数学技术”是不合适的,因为它不可见且不可维护。那将是一个坏习惯。抱歉,我使用的技术对我来说是非常明显和可维护的-因为六个月后当我返回代码时,我会清楚地理解代码,而不会看到一团混乱的意大利面条。

嘿哥们(就像以前的一位前客户所说的)会做您想做的事情,只要您知道如何解决它,当我需要您修复它时。

我记得20年前,我的一位同事因采用今天称为敏捷开发策略的工作而被解雇。他有一个细致的增量计划。但是他的经理对他大吼:“您不能向用户逐步发布功能!您必须坚持瀑布式设计。” 他对经理的回应是,渐进式开发将更精确地满足客户的需求。他相信要开发满足客户需求的产品,但经理相信要按照“客户要求”进行编码。

我们经常因违反数据规范化,MVPMVC界限而感到内gui。我们内联而不是构造一个函数。我们采取捷径。

我个人认为PHP是不好的做法,但是我知道什么。所有理论上的争论归结为试图满足一套规则

质量=精度,可维护性和盈利能力。

所有其他规则逐渐淡出背景。当然,这条规则永远不会消失:

懒惰是优秀程序员的美德。


1
“嘿,伙计(就像以前的一位前客户所说的那样)可以做您想要的,只要您知道如何在我需要它修复时就可以修复它。” 问题:通常不是您来“修复”它。
丹·巴伦

对此答案+1,因为我同意您的去向,但不一定同意您的去向。我认为存在一定程度的理解。也就是说,员工A在5年的编程经验和5年的公司工作经验后,与员工B(刚从公司开始的新大学毕业生)的理解截然不同。我的观点是,如果员工A是唯一能够理解代码的人,那么它就无法维护,因此我们都应该努力编写员工B可以理解的代码。这就是软件中真正的艺术所在。
弗朗西斯·罗杰斯2014年

9

我倾向于使用保护子句早返回,否则在方法结束时退出。单一的进入和退出规则具有历史意义,对于处理具有多个返回(和许多缺陷)的单个C ++方法运行到10个A4页的遗留代码特别有用。最近,公认的良好实践是使方法保持较小,这样可以减少多个出口对理解的阻抗。在从上面复制的以下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#中)在Guard子句中抛出ArgumentNullException而不是返回null值。
David Clarke 2013年

7

我可以想到的一个很好的理由是代码维护:您只有一个出口。如果您想更改结果的格式,...,它的实现要简单得多。另外,为了进行调试,您可以在此处放置一个断点:)

话虽如此,我曾经不得不在一个编码标准强加“每个函数一个返回语句”的库中工作,我发现这非常困难。我写了很多数值计算代码,并且经常有“特殊情况”,所以代码最终很难遵循...


那并没有真正的改变。如果更改返回的局部变量的类型,则必须将所有分配固定到该局部变量。无论如何,定义一个具有不同签名的方法可能更好,因为您还必须修复所有方法调用。
Maarten Bodewes

@ MaartenBodewes-owlstead-它可以有所作为。仅举两个示例,您不必将所有分配都固定到局部变量或更改方法调用,该函数可能以字符串形式返回日期(局部变量将是实际日期,仅在日期处格式化为字符串)最后一刻),否则它可能会返回一个十进制数字,而您想更改小数位数。
nnnnnn 2015年

@nnnnnn好的,如果您想对输出进行后期处理...但是我只是生成一个新的方法来进行后期处理,而将旧的方法搁置一旁。这只是稍微更难重构,但你必须检查其它呼叫与新格式兼容反正。但这仍然是一个正当的理由。
Maarten Bodewes,2015年

7

多个出口点适用于足够小的功能-也就是说,可以在一个屏幕长度上整体查看的功能。如果冗长的函数同样包含多个退出点,则表明该函数可以进一步切分。

也就是说,除非绝对必要,否则我将避免使用多次退出功能。我感到有些错误是由于在更复杂的函数中某些模糊的行中出现了一些杂散返回而引起的。


6

我曾使用过糟糕的编码标准,这些标准强制您使用一个退出路径,并且如果该功能除琐碎的事情之外,结果几乎总是非结构化的意大利面-您最终会遇到很多麻烦,并继续遇到麻烦。


更不用说必须告诉您的心跳过 if每个返回成功与否的方法调用前语句:(
Maarten Bodewes

6

单一出口点-在所有其他条件相同的情况下-使代码可读性大大提高。但是有一个陷阱:受欢迎的建筑

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

是假的,“ res =”并不比“ return”好得多。它具有单个return语句,但是函数实际上在多个点结束。

如果您的函数具有多个返回值(或“ res =“ s”),则通常最好将其分解为几个具有单个退出点的较小函数。


6

我通常的策略是在一个函数的末尾只有一个return语句,除非通过添加更多代码来大大降低代码的复杂性。实际上,我是Eiffel的粉丝,它通过没有return语句(只有自动创建的“结果”变量来放入结果)来强制执行唯一的返回规则。

当然,在某些情况下,可以使多次返回的代码比没有它们的明显版本更清晰。有人可能会争辩说,如果您的函数过于复杂而无法在没有多个return语句的情况下被理解,则需要做更多的工作,但是有时务实地对待此类事情是件好事。


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

最后,我投票给Single Return作为指导。这有助于进行常见的代码清理操作。例如,看下面的代码...

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

您为单收益投票-C代码。但是,如果您使用具有垃圾回收功能并尝试..finally块的语言进行编码怎么办?
安东尼2010年

4

这可能是一个不同寻常的观点,但是我认为,任何认为应该支持多个return语句的人都不必在仅支持4个硬件断点的微处理器上使用调试器。;-)

尽管“箭头代码”的问题是完全正确的,但是在使用多个返回语句时似乎已经消失的一个问题是使用调试器的情况。您没有方便的通用位置放置断点以确保您将看到出口以及返回条件。


5
那只是另一种过早的优化。您绝对不应针对特殊情况进行优化。如果您发现自己经常调试代码的特定部分,那么问题不仅仅在于它有多少出口点。

也取决于您的调试器。
Maarten Bodewes

4

函数中的return语句越多,该方法的复杂度就越高。如果您想知道自己是否有太多的return语句,则可能要问自己是否在该函数中有太多的代码行。

但是,不是,一个/很多return语句没有错。在某些语言中,这是比其他语言(C)更好的做法(C ++)。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.