我经常和程序员说“不要在同一方法中放入多个return语句”。当我要求他们告诉我原因的时候,我得到的只是“编码标准是这样的”。或“令人困惑”。当他们通过单个return语句向我展示解决方案时,代码对我来说看起来更难看。例如:



 if (condition)
   return 42;
else
   return 97;
 


”“这很丑,您必须使用局部变量!“”

 int result;
if (condition)
   result = 42;
else
   result = 97;
return result;
 


这50%的代码如何膨胀使程序更容易理解?我个人觉得比较难,因为状态空间刚刚增加了另一个很容易避免的变量。

当然,通常我会写: preclass =“ lang-c prettyprint-override”> return (condition) ? 42 : 97;

但是许多程序员避开了条件运算符,而更喜欢长格式。

这个概念在哪里的“仅退货”从何而来?产生此约定是否有历史原因?

评论

这在某种程度上与Guard Clause重构有关。 stackoverflow.com/a/8493256/679340保护子句会将返回值添加到方法的开头。在我看来,它使代码更简洁。

它来自结构化编程的概念。有人可能会争辩说,只有一次返回就可以让您轻松地修改代码以在返回之前做一些事情,或者轻松地进行调试。

我认为这个例子很简单,我不会以一种方式或其他方式产生强烈的意见。单入单出的理想状态更多地是指导我们摆脱疯狂的局面,例如15条返回语句和另外两个根本不返回的分支!
那是我读过的最糟糕的文章之一。似乎作者花了更多时间幻想自己的OOP的纯度,而不是真正弄清楚如何实现任何目标。表达式树和评估树具有价值,但是当您仅编写普通函数时就没有价值。

您应该完全删除条件。答案是42。

#1 楼

当大多数编程是使用汇编语言,FORTRAN或COBOL完成时,编写了“单入口,单出口”。它已被广泛误解,因为现代语言不支持Dijkstra所警告的做法。

“单项输入”的意思是“不要为功能创建替代的入口点”。当然,用汇编语言,可以在任何指令下输入功能。 FORTRAN通过ENTRY语句支持函数的多个条目:“单出口”表示一个函数应仅返回一个位置:紧随调用之后的语句。这并不意味着一个函数只能从一个地方返回。编写结构化编程时,通常的做法是,函数通过返回备用位置来指示错误。 FORTRAN通过“备用返回”支持此操作:

      SUBROUTINE S(X, Y)
      R = SQRT(X*X + Y*Y)
C ALTERNATE ENTRY USED WHEN R IS ALREADY KNOWN
      ENTRY S2(R)
      ...
      RETURN
      END

C USAGE
      CALL S(3,4)
C ALTERNATE USAGE
      CALL S2(5)


这两种技术都容易出错。使用备用条目通常会使一些变量未初始化。使用备用返回具有GOTO语句的所有问题,另外一个复杂的问题是分支条件与分支不相邻,而是在子例程中的某个地方。纸。请参阅http://www.cs.utexas.edu/users/EWD/ewd02xx/EWD249.PDF,第28页(打印页数为24)。不限于功能。

评论


并且不要忘记意大利面条代码。子例程使用GOTO(而不是返回)退出,将函数调用参数和返回地址保留在堆栈中,这并非未知。单出口被提升为至少将所有代码路径集中到RETURN语句的一种方式。

– TMN
2011-11-10 15:52

@TMN:早期,大多数机器没有硬件堆栈。通常不支持递归。子例程参数和返回地址存储在与子例程代码相邻的固定位置。 Return只是间接的goto。

–kevin cline
2011年11月10日下午16:33

@kevin:是的,但是根据您的说法,这甚至不再意味着它的初衷。 (顺便说一句,实际上我有理由确定弗雷德问的是对“单出口”的当前解释的偏好来自何方。)而且,自从这里的许多用户诞生之前,C就已经有了const,因此不需要大写常量。甚至在C语言中也是如此。但是Java保留了所有那些不良的C语言旧习惯。

–sbi
2011年11月10日22:08

那么异常是否违反了对“单出口”的解释? (或者他们更原始的表亲,setjmp / longjmp?)

–梅森·惠勒
2011年11月22日,1:13

即使操作员询问了有关单收益的当前解释,但该答案还是具有最悠久历史根源的答案。除非您希望您的语言与VB(而不是.NET)的出色表现相匹配,否则使用单条返回没有任何意义。只需记住也要使用非短路布尔逻辑。

–人才
2012年11月29日,0:44

#2 楼

单入口单出口(SESE)的概念来自具有显式资源管理的语言,例如C和汇编语言。在C语言中,这样的代码会泄漏资源:

 void f()
{
  resource res = acquire_resource();  // think malloc()
  if( f1(res) )
    return; // leaks res
  f2(res);
  release_resource(res);  // think free()
}
 


在这种语言中,您基本上有三个选项:


复制清理代码。冗余总是不好的。
使用goto跳转到清理代码。
这要求清理代码成为函数中的最后一件事。 (这就是为什么有人认为goto占有一席之地。它的确在C中存在。)
引入局部变量并通过该变量操纵控制流。
缺点是控制流通过语法进行操纵(认为​​breakreturnifwhile)比通过变量状态操纵的控制流容易遵循(因为当您查看算法时,这些变量没有状态)。

在汇编中甚至更奇怪,因为调用该函数时您可以跳转到函数中的任何地址,这实际上意味着您几乎可以无限地访问任何函数的入口点。 (有时这很有用。此类重击是编译器实现this指针调整所必需的常见技术,该指针调整是在C ++中的多继承方案中调用virtual函数所必需的。)

当您必须手动管理资源时,利用在任何地方进入或退出函数的选项都会导致更复杂的代码,从而导致错误。因此,出现了一种传播SESE的思想流派,目的是获得更简洁的代码和更少的错误。


但是,当一种语言具有异常功能时,(几乎)任何功能都可能在(几乎)任何点过早退出,因此无论如何您都需要为过早返回做好准备。 (我认为finally主要用于Java中,using(在实现C43时实现IDisposablefinally时); C ++使用RAII。)完成此操作后,由于早期的return语句,您将无法自行清理,因此支持SESE的最强论据可能消失了。

留下可读性。当然,带有六个return语句的200 LoC函数随机散布在该函数上并不是很好的编程风格,也无法编写可读的代码。但是,如果没有这些过早的返回,这样的功能也将不容易理解。

在没有或不应该手动管理资源的语言中,坚持旧的SESE几乎没有价值惯例。就像我上面所说的,OTOH,SESE通常会使代码更加复杂。这是一种恐龙(除C之外)不能很好地适应当今的大多数语言。它没有帮助提高代码的易懂性,反而阻碍了它。


为什么Java程序员坚持这样做呢?我不知道,但是从我的POV(外部)来看,Java从C(它们有意义的地方)采纳了许多约定,并将它们应用于其OO世界(在这里它们是无用的或完全坏的),现在它遵循他们,无论付出什么代价。 (按照惯例,在作用域的开头定义所有变量。)

出于非理性的原因,程序员会坚持使用各种奇怪的符号。 (深层嵌套的结构化语句(“箭头”)曾在Pascal之类的语言中被视为漂亮的代码。)对此应用纯逻辑推理似乎无法说服大多数人偏离其既定方式。改变这种习惯的最好方法可能是早日教他们做最好的事情,而不是常规的事情。作为编程老师,您可以掌握它。 :)

评论


对。在Java中,清理代码属于finally子句,无论早期返回或异常如何,清理子代码都将在其中执行。

– dan04
11年9月9日在9:41

在Java 7中,@ dan04在大多数时间甚至都不需要。

– R. Martinho Fernandes
2011年11月9日在9:45

@Steven:当然可以证明这一点!实际上,您可以使用任何功能来显示复杂且复杂的代码,这些功能也可以使代码更简单易懂。一切都可能被滥用。关键是编写代码以便于理解,而这时就要把SESE扔到窗外,该死,并责骂适用于不同语言的旧习惯。但是,如果我认为它使代码更易于阅读,我也将毫不犹豫地控制变量的执行。只是我不记得在近二十年内见过这样的代码。

–sbi
2011年11月9日,12:31

@Karl:的确,这是Java之类的GC语言的严重缺陷,它们使您不必清理一种资源,而使所有其他资源都无法使用。 (C ++使用RAII解决了所有资源的此问题。)但我什至不只在谈论内存(我只在示例中只添加了malloc()和free()作为注释),而我只是在谈论资源。我也不是说GC可以解决这些问题。 (我确实提到过C ++,它没有开箱即用的GC。)据我了解,最终使用Java解决了这个问题。

–sbi
2011年11月9日14:15

@sbi:对于功能(过程,方法等)而言,比起不超过一页长更重要的是该功能具有明确定义的契约;如果由于为了满足任意长度限制而将其切碎而没有做清楚的事情,那就不好了。编程是要互相作用,有时是相互冲突。

–研究员
2011-11-22 14:20

#3 楼

一方面,单个return语句使日志记录以及依赖日志记录的调试形式更加容易。我记得很多次我不得不将函数简化为单返回值,只是为了单点打印出返回值。

   int function() {
     if (bidi) { print("return 1"); return 1; }
     for (int i = 0; i < n; i++) {
       if (vidi) { print("return 2"); return 2;}
     }
     print("return 3");
     return 3;
  }
 


另一方面,您可以将其重构为function(),它调用_function()并记录结果。

评论


我还要补充一点,它使调试更加容易,因为您只需要设置一个断点即可捕获该函数的所有出口*。我相信有些IDE可以让您在函数的右括号中放置一个断点来执行相同的操作。 (*除非您致电出口)

– Skizz
2011年11月10日11:03

出于类似的原因,这也使扩展(添加)功能变得更加容易,因为不必在每次返回之前插入新功能。例如,假设您需要使用函数调用的结果来更新日志。

– JeffSahol
11年10月10日在16:43

老实说,如果我要维护该代码,我宁愿使用一个明智定义的_function(),并在适当的位置返回,并使用一个名为function()的包装程序来处理无关的日志记录,而不是使用具有扭曲逻辑的单个function()使所有返回值都适合单个出口点,以便我可以在该点之前插入一条附加语句。

–ruakh
2011年11月13日19:41

在某些调试器(MSVS)中,您可以在最后一个右括号上放置断点

– Abyx
11年11月21日在21:56

打印!=调试。那根本不是争论。

–彼得·霹雳
13年12月14日在9:08

#4 楼

“单入口,单出口”起源于1970年代初期的结构化编程革命,该革命由Edsger W. Dijkstra致编辑的信GOTO认为有害的声明开始。在Ole Johan-Dahl,Edsger W. Dijkstra和Charles Anthony Richard Hoare的经典著作《结构化编程》中详细阐述了结构化编程的概念。

必须阅读“ GOTO声明被认为有害” , 即使在今天。 “结构化程序设计”已经过时,但是仍然非常非常有益,并且应该在任何开发人员的“必读”列表的顶部,远高于例如史蒂夫·麦康奈尔。 (Dahl的部分介绍了Simula 67中的类的基础知识,它们是C ++和所有面向对象程序设计的技术基础。)

评论


本文是在C语言被广泛使用的C语言之前的几天编写的。他们不是敌人,但是这个答案肯定是正确的。不在函数末尾的return语句实际上是goto。

–user606723
2011年11月9日15:21

这篇文章也是在goto可以随便去哪儿的日子写的,就像直接进入另一个函数中的某个随机点,绕过了过程,函数,调用栈等的任何概念。如今,没有理智的语言允许使用goto直接进行。 C的setjmp / longjmp是我所知道的唯一的半例外情况,即使这也需要双方的合作。 (不过,半讽刺的是,考虑到异常的作用几乎相同,所以我在那儿使用了“例外”一词。)基本上,本文不鼓励长期以来已经死去的实践。

– cHao
2011年11月9日15:41



从“ Goto语句被认为是有害的”的最后一段开始:“在[2]中,Guiseppe Jacopini似乎证明了go语句的(逻辑)多余之处。该练习或多或少地将任意流程图机械地转换为一个跳转,但是,不建议使用较少的流程图。因此,不能期望结果流程图比原始流程图更透明。”

–hugomg
2011年11月9日18:22



这与问题有什么关系?是的,Dijkstra的工作最终导致了SESE语言的发展,那又如何呢?巴贝奇的工作也是如此。并且,如果您认为该文件说明了某个函数中具有多个退出点的任何内容,那么也许应该重新阅读该文件。因为不是。

–杰夫
2011年11月9日18:35

@John,您似乎在尝试回答问题而不实际回答。这是一个不错的阅读清单,但您没有引用也没有用任何措辞来证明您的观点,即这篇文章和书中有关于烟民关注的话题。确实,除了评论之外,您没有对这个问题发表任何实质性的意见。考虑扩大这个答案。

–Shog9
2011年11月9日在22:05

#5 楼

链接Fowler总是很容易。

违反SESE的主要示例之一是保护子句:

用保护子句替换嵌套条件


使用保护条款所有特殊情况

<预类= “郎-C prettyprint-越权”> double getPayAmount() { double result; if (_isDead) result = deadAmount(); else { if (_isSeparated) result = separatedAmount(); else { if (_isRetired) result = retiredAmount(); else result = normalPayAmount(); }; } return result; };



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


有关更多信息,请参见第250页的“重构”。


评论


另一个不好的例子:使用else-ifs可以很容易地修复它。

–杰克
15年7月6日在17:58

您的示例不公平,如何处理:double getPayAmount(){double ret = normalPayAmount();如果(_isDead)ret = deadAmount();如果(_isSeparated)ret = splitAmount();如果(_isRetired)ret = retiredAmount();返回ret };

– Charbel
16-4-22在6:55



@Charbel这不是同一回事。如果_isSeparated和_isRetired都可以为真(为什么不可以呢?),则返回错误的金额。

–hvd
17年1月22日在9:02

@Konchog“嵌套条件将比保护子句提供更好的执行时间”这主要需要引用。我怀疑这确实是真的。例如,在这种情况下,就生成的代码而言,早期返回与逻辑短路有何不同?即使很重要,我也无法想象这种差异将不只是无限小条。因此,您通过降低代码的可读性来进行过早的优化,只是为了满足有关您认为会导致代码稍快一些的未经验证的理论观点。我们不在这里

– underscore_d
18-09-23在16:09



@underscore_d,您是对的。它很大程度上取决于编译器,但会占用更多空间。查看两个伪程序集,很容易看出为什么保护子句来自高级语言。 “ A”测试(1); branch_fail结束;测试(2); branch_fail结束;测试(3); branch_fail结束; {CODE}结尾:返回; “ B”检验(1); branch_good next1;返回; next1:test(2); branch_good next2;返回; next2:test(3); branch_good next3;返回; next3:{CODE}返回;

–user269891
18-09-24在11:32



#6 楼

我不久前写了一篇有关该主题的博客文章。

最重要的是,该规则来自没有垃圾收集或异常处理的语言时代。没有正式的研究表明该规则可以导致现代语言中更好的代码。只要这会导致代码更短或更易读,就可以忽略它。坚持这一点的Java专家遵循过时的,毫无意义的规则,对此盲目且毫无疑问。

Stackoverflow上也曾问过这个问题

评论


嗨,我再也无法访问该链接了。您是否碰巧仍然可以访问某个地方托管的版本?

–莫妮卡基金的诉讼
17年1月22日在4:12

嗨,QPT,好地方。我把博客文章带回来并更新了上面的URL。现在应该链接!

–安东尼
17年1月28日在18:23

不仅如此,它还有更多。使用SESE管理精确的执行时间要容易得多。无论如何,嵌套条件条件通常都可以通过开关进行重构。这不仅仅是关于是否有返回值。

–user269891
17年4月18日在7:15

如果您要声明没有正式的研究来支持它,那么您应该链接到与之相反的研究。

–user541686
17年6月12日在13:41



Mehrdad,如果有正式研究支持它,请显示出来。就这样。坚持反对证据正在转移举证责任。

–安东尼
17年6月13日在8:10



#7 楼

一回就可以简化重构。尝试对包含返回,中断或继续的for循环的内部执行“提取方法”。这将因为您破坏了控制流程而失败。

要点是:我想没有人会假装编写完美的代码。因此,代码在重构时通常会被“改进”和扩展。因此,我的目标是保持代码尽可能友好的重构。

我经常遇到这样的问题:如果函数包含控制流破坏者并且我只想添加一点点功能,则必须完全重新编写函数。当您更改整个控制流程而不是向孤立的嵌套引入新路径时,这很容易出错。如果最后只有一个返回,或者如果使用了防护退出循环,那么您当然会有更多的嵌套和更多的代码。但是您可以获得编译器和IDE支持的重构功能。

评论


变量也一样。这是使用控制流结构(如早期返回)的替代方法。

–重复数据删除器
17-10-16在22:07

变量通常不会妨碍您以保留现有控制流的方式将代码分成几部分。尝试“提取方法”。由于IDE无法从您编写的内容中派生语义,因此它们仅能够执行控制流预派生重构。

–oopexpert
17-10-17在7:12

#8 楼

考虑以下事实:多个return语句等同于将GOTO包含到单个return语句中。这与break语句相同。因此,像我一样,有些人出于所有意图和目的都将它们视为GOTO。我找到了一个很好的理由。

我的一般规则是GOTO仅用于流控制。绝对不要将它们用于任何循环,也永远不要转到“向上”或“向后”。 (中断/返回的工作方式)

正如其他人提到的,以下内容必须阅读
被认为有害的GOTO声明
,但是请记住,这是用1970年,GOTO被过度使用。并非每个GOTO都是有害的,只要您不使用它们而不是常规构造,我就不会劝阻它们的使用,但是在奇怪的情况下,使用常规构造会非常不便。

我发现在因错误而需要逃生的错误情况下使用它们,在正常情况下有时永远不会发生这种错误有时很有用。但是,您还应该考虑将此代码放到一个单独的函数中,以便您可以提早返回而不是使用GOTO ...,但是有时这也不方便。

评论


替换goto的所有结构化构造都是根据goto实现的。例如。循环,“ if”和“ case”。这不会使它们变坏-实际上相反。同样,它是“意图和目的”。

–安东尼
2011年11月10日15:05



Touche,但这与我的观点没有不同……这只是使我的解释有些错误。那好吧。

–user606723
2011年11月10日下午16:33

只要(1)目标位于相同的方法或函数中,并且(2)代码中的方向是向前的(跳过某些代码),并且(3)目标不在其他嵌套结构中(例如,GOTO),GOTO应该始终可以从if-case的中间转到else-case的中间)。如果遵循这些规则,则所有在GOTO上的滥用都会在视觉和逻辑上产生强烈的代码气味。

– Mikko Rantalainen
2012-12-11 11:29

#9 楼

圈复杂度

我已经看到SonarCube使用多重返回语句来确定圈复杂度。因此,返回语句越多,循环复杂度就越高。

返回类型更改

多个返回意味着我们在决定更改返回值时需要在函数的多个位置进行更改类型。

多个出口

调试起来比较困难,因为需要结合条件语句仔细研究逻辑,以了解导致返回值的原因。
重构解决方案

解决多个return语句的方法是在解决所需的实现对象后,将其替换为具有单条返回的多态。

评论


从多个退货转移到在多个位置设置退货值并不能消除圈复杂度,而只是统一了出口位置。在给定的上下文中,圈复杂性可以表明的所有问题仍然存在。 “很难调试,因为需要结合条件语句仔细研究逻辑,以了解导致返回值的原因。”同样,通过统一返回值,逻辑不会改变。如果您必须仔细研究代码以了解其工作原理,则需要对其进行重构,这是一个完整的过程。

– WillD
19年3月29日在18:37



#10 楼


产生该公约是否有历史原因?用数学证明代码的正确性。这被认为是足够重要的,因此它是课程的一部分。

这是来自美国康奈尔大学(2004)的最新文章:https://www.cs.cornell.edu/ course / cs312 / 2004fa / lectures / lecture9.htm

语言对此至少没有影响。您断言什么是真或不是真的能力更多。

评论


我想我记得70年代末80年代初ACM撰写的SIGPLAN文章中有关正确性证明系统的文章,其中说单项回报使这项工作变得容易得多(以及诸如断言之类的其他装饰)。当然,我们知道这项努力的结果:学到了很多关于语言设计的知识,但最终并没有实用的工具。我还记得那些文章中证明者和测试者之间的斗争。我们知道谁赢得了这场辩论。

– Jeff Learman
6月10日15:45

#11 楼

多次返回在代码中引入了隐式路径,这些隐式路径恰好超过了函数(或方法)的末尾,这些路径在单个return语句或函数末尾都不可见。这可能会导致审阅者错误地解释代码的行为,而不会造成自己的过错。并非一眼就可以检查所有功能,并且必须检查其他返回路径会增加错误的可能性。

其中的符号被用作Guard子句的一种形式,其中包括Smalltalk以及其他是根据合同前提条件进行设计的一种形式。在这种情况下,只建议他们使用语法,因为它们不会改变功能的行为。每个前提条件都会对连续执行实施否决权,其顺序并不重要。

在归因于Fowler的情况下,这种方法不太可取。在确定性地评估防护以选择行为的情况下,返回值使代码看起来像先决条件,但并非如此,并且取决于其顺序。这引入了OP标识为与所添加变量有关的状态依赖关系。嵌套的if / else语句将使案例的分类变得明确并简化理解。返回值再次成为语法标记,其他符号可以删除。这等效于Horn子句,并提供了一个区分程序中多个路径的安全模型。

在所有情况下,正确的代码胜过漂亮的代码。

#12 楼

今天,有一个实际的原因:使调试更容易。如果要查看函数返回哪个值,通常最容易将结果存储在最后返回的变量中,因此可以设置单个断点并检查返回值。或打印它进行调试。如果您拥有可以做得更好的调试器,那么对您有好处。拥有一个调试器非常理想,该调试器可让您在函数的右括号中设置断点,但实际上会在返回的return语句处中断并显示值。

评论


在调用函数的原点处设置一个断点,并检查该处的返回值。

–罗伯特·哈维(Robert Harvey)
6月10日16:51



罗伯特,我期望您能更好。 f(self.prop1,self.prop2,self.prop3)。现在设置一个断点以获取prop2的返回值。

– gnasher729
6月11日下午13:48

颠覆整个编码风格还不是一个足够好的理由。提早退出是一种非常有用的技术。它清理了无尽的if-else梯子,并大大简化了逻辑。

–罗伯特·哈维(Robert Harvey)
6月11日下午13:53

由每个人自己决定。不过,您的第一句话是胡说八道。

– gnasher729
6月11日13:56