是否有充分的理由说明为什么在函数中只有一个return语句是更好的做法?
或者,一旦逻辑正确就可以从函数返回,这意味着函数中可能有很多返回语句?
我经常在一个方法的开头有几个语句来返回"简单"的情况.例如,这个:
public void DoStuff(Foo foo)
{
if (foo != null)
{
...
}
}
......可以像这样更具可读性(恕我直言):
public void DoStuff(Foo foo)
{
if (foo == null) return;
...
}
所以,是的,我认为从函数/方法中获得多个"退出点"是很好的.
没有人提到或引用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语句.这是为了限制复杂性.许多人如Martin Fowler认为用多个return语句编写函数更简单.他在他写的经典重构书中提出了这个论点.如果你遵循他的其他建议并编写小函数,这很有效.我同意这种观点,只有严格的结构化编程纯粹主义者遵守每个函数的单个返回语句.
正如肯特贝克在实施模式中讨论保护条款时所说的那样,使例程有一个单一的入口和出口点......
"这是为了防止在同一例程中进出许多位置时出现混淆.当应用于FORTRAN或汇编语言程序时,它很有意义,这些程序用大量全局数据编写,甚至理解执行哪些语句也很难.用小方法和大多数本地数据,这是不必要的保守."
我发现一个用guard子句编写的函数比一个长嵌套的if then else
语句更容易理解.
在没有副作用的函数中,没有充分理由获得多个返回值,您应该以函数式编写它们.在具有副作用的方法中,事物更顺序(时间索引),因此您使用return语句作为命令停止执行,以命令式方式编写.
换句话说,如果可能的话,赞成这种风格
return a > 0 ? positively(a): negatively(a);
在此
if (a > 0) return positively(a); else return negatively(a);
如果您发现自己编写了几层嵌套条件,那么可能有一种方法可以使用谓词列表进行重构.如果您发现ifs和elses在语法上相距甚远,您可能希望将其分解为更小的函数.跨越多个屏幕文本的条件块很难阅读.
没有适用于每种语言的硬性规则.像一个返回语句之类的东西不会使你的代码变好.但好的代码往往会让你以这种方式编写你的函数.
我已经在C++的编码标准中看到了它,这是C语言的遗留问题,好像你没有RAII或其他自动内存管理那么你必须清理每次返回,这或者意味着剪切和粘贴清理或goto(逻辑上与托管语言中的'finally'相同),这两种都被认为是不良形式.如果你的做法是在C++或其他自动内存系统中使用智能指针和集合,那么就没有强有力的理由,它就变成了可读性和更多的判断调用.
我倾向于认为函数中间的返回语句很糟糕.你可以使用return在函数的顶部构建一些guard子句,当然告诉编译器在函数结束时返回什么没有问题,但是函数中间的返回很容易错过并且可以使功能更难解释.
是否有充分的理由说明为什么在函数中只有一个return语句是更好的做法?
是的,有:
单一出口点是断言后期条件的绝佳场所.
能够在函数末尾的一个返回上放置调试器断点通常很有用.
回报越少意味着复杂性越低.线性代码通常更容易理解.
如果试图将函数简化为单个返回会导致复杂性,那么这就是重构更小,更通用,更容易理解的函数的动机.
如果您使用的是没有析构函数的语言,或者如果您不使用RAII,那么单次返回会减少您必须清理的地点数量.
某些语言需要单个退出点(例如,Pascal和Eiffel).
这个问题通常被认为是多个回报或深度嵌套的if语句之间的错误二分法.几乎总有第三种解决方案非常线性(没有深度嵌套),只有一个出口点.
更新:显然,MISRA指南也促进单一退出.
要明确的是,我并不是说多次退货总是错的.但是考虑到其他方面相同的解决方案,有很多理由可以选择具有单一回报的解决方案.
拥有单一出口点确实在调试提供了一个优势,因为它可以让你在一个函数的末尾设置一个断点,单看什么样的价值实际上是要被退回.
一般来说,我尝试从函数中只有一个退出点.然而,有时候这样做实际上最终会创建一个比必要的更复杂的函数体,在这种情况下,最好有多个出口点.它必须是基于由此产生的复杂性的"判断调用",但目标应该是尽可能少的出口点而不牺牲复杂性和可理解性.
我倾向于单一退出,除非它确实使事情复杂化.我发现在某些情况下,多个存在点可以掩盖其他更重要的设计问题:
public void DoStuff(Foo foo)
{
if (foo == null) return;
}
看到这段代码后,我会马上问:
'foo'永远是空的吗?
如果是这样,'DoStuff'有多少客户端使用null'foo'调用该函数?
根据这些问题的答案,可能是这样
检查是没有意义的,因为它永远不是真的(即它应该是一个断言)
检查很少是真的,所以最好更改那些特定的调用函数,因为他们应该采取其他一些行动.
在上述两种情况下,代码都可以通过断言重新编写,以确保'foo'永远不会为空并且相关的调用者发生了变化.
还有另外两个原因(具体我认为是C++代码),其中多个存在实际上可能产生负面影响.它们是代码大小和编译器优化.
函数出口范围内的非POD C++对象将调用其析构函数.如果有多个返回语句,则可能是范围内有不同的对象,因此要调用的析构函数列表将不同.因此编译器需要为每个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() );
}
只需按原样执行代码,对象'a1'在'foo'中构造,然后调用其复制构造来构造'a2'.但是,copy elision允许编译器在堆栈的同一位置构造'a1'作为'a2'.因此,当函数返回时,不需要"复制"对象.
多个出口点使编译器在尝试检测到这一点时的工作变得复杂,并且至少对于VC++的相对较新版本,优化不会发生在函数体具有多个返回的情况下.有关更多详细信息,请参阅Visual C++ 2005中的命名返回值优化.
不,因为我们不再生活在20世纪70年代了.如果你的功能足够长,多次返回是一个问题,那就太长了.
(除了事实上,具有例外的语言中的任何多行函数无论如何都会有多个出口点.)
具有单个退出点可降低Cyclomatic Complexity,因此理论上可以降低在更改代码时将错误引入代码的可能性.然而,实践往往表明需要更务实的方法.因此,我倾向于只有一个退出点,但如果可读性更高,则允许我的代码有几个.
我强迫自己只使用一个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;
}
(条件是arbritary ......)
条件越多,函数越大,读取的难度就越大.因此,如果你对代码气味很熟悉,你就会意识到这一点,并希望重构代码.两种可能的解决方案是
多次退货
重构为单独的函数
多次退货
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);
}
当然,它更长,有点乱,但在这种方式重构功能的过程中,我们已经
创建了许多可重用的函数,
使功能更具人性化,并且
功能的重点是价值为何正确.
我会说你应该有尽可能多的,或任何使代码更清晰(如保护条款).
我个人从未听过/看到任何"最佳实践"说你应该只有一个回复声明.
在大多数情况下,我倾向于根据逻辑路径尽快退出函数(保护子句就是一个很好的例子).
有一个单一的退出点有很好的说法,就像对不可避免的"箭头"编程有不好的说法一样.
如果在输入验证或资源分配期间使用多个退出点,我会尝试将所有"错误退出"非常明显地放在函数的顶部.
"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非常容易单独测试.
显然,在一个支持OO和异常的语言中,我不会使用像这样的if语句(或者根本不使用if,如果我能用很少的努力侥幸逃脱它),代码就会更加清晰.并且非箭头.大多数非最终回报可能都是例外.
简而言之;
很少有回报比许多回报更好
不止一次回归比巨箭更好,保护条款通常都可以.
在可能的情况下,例外可能/应该取代大多数"保护条款".
我相信多次返回通常都很好(在我用C#编写的代码中).单回归风格是C的保留.但你可能不是用C编码.
在所有编程语言中,没有法律只要求一个方法的出口点.有些人坚持这种风格的优越性,有时他们将其提升为"规则"或"法律",但这种信念并没有任何证据或研究支持.
C代码中不止一种返回样式可能是一种坏习惯,其中资源必须被显式解除分配,但Java,C#,Python或JavaScript等语言具有自动垃圾收集和try..finally
块(以及using
C#中的块)等构造),这个论点不适用 - 在这些语言中,需要集中的手动资源释放是非常罕见的.
有些情况下,单个返回更易读,而有些情况则不可读.看看它是否减少了代码行数,使逻辑更清晰或减少了大括号和缩进或临时变量的数量.
因此,使用尽可能多的回报以适应您的艺术感受,因为它是布局和可读性问题,而不是技术问题.
我在博客上已经详细讨论了这个问题.
你知道这句谚语 - 美丽在旁观者的眼中.
有些人发誓NetBeans,有些人用IntelliJ IDEA发誓,有些人用Python发誓,有些人用PHP发誓.
在某些商店,如果你坚持这样做,你可能会失去工作:
public void hello()
{
if (....)
{
....
}
}
问题在于可见性和可维护性.
我沉迷于使用布尔代数来减少和简化逻辑和状态机的使用.然而,过去有些同事认为我在编码中使用"数学技术"是不合适的,因为它不可见和可维护.这将是一个糟糕的做法.对不起的人,我使用的技术对我来说是非常明显和可维护的 - 因为当我六个月后回到代码时,我会清楚地理解代码,而不是看到一堆众所周知的意大利面条.
嘿伙计(就像以前的客户曾经说过的那样)做你想做的事,只要你知道如何解决它我需要你解决它.
我记得20年前,我的一位同事因雇用今天所谓的敏捷发展战略而被解雇.他有一个细致的增量计划.但他的经理却对他大吼大叫"你无法逐步向用户发布功能!你必须坚持使用瀑布." 他对经理的回应是增量开发会更准确地满足客户的需求.他相信为客户需求开发,但经理相信编码符合"客户的要求".
我们经常因打破数据规范化,MVP和MVC边界而感到内疚.我们内联而不是构建一个函数.我们采取捷径.
就个人而言,我认为PHP是不好的做法,但我知道什么.所有的理论论点都归结为尝试实现一套规则
质量=精确性,可维护性和盈利能力.
所有其他规则都淡入背景.当然,这条规则永远不会消退:
懒惰是优秀程序员的优点.
我倾向于使用保护条款提前返回,否则在方法结束时退出.单个进入和退出规则具有历史意义,在处理具有多个返回(以及许多缺陷)的单个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;
}
我能想到的一个很好的理由是代码维护:你只有一个退出点.如果你想改变结果的格式,......,它实现起来要简单得多.另外,为了调试,你可以在那里坚持一个断点:)
话虽如此,我曾经不得不在一个库中工作,其中编码标准强加了"每个函数一个返回语句",我发现它非常难.我写了很多数值计算代码,经常有"特殊情况",所以代码最终很难遵循......
多个出口点适用于足够小的功能 - 也就是说,可以在一个屏幕上查看整个功能.如果冗长的功能同样包括多个出口点,则表明该功能可以进一步切断.
这就是说除非绝对必要,否则我会避免多次退出功能.我感到很多错误的痛苦,这些错误是由于在更复杂的功能中某些模糊的线路中的一些偏离返回.
我已经使用了可怕的编码标准,迫使你只有一个退出路径,结果几乎总是非结构化的意大利面,如果这个功能不算什么微不足道的话 - 你最终会有很多休息时间而且会继续阻碍你.
单一退出点 - 所有其他条件相同 - 使代码更具可读性.但有一个问题:流行建筑
resulttype res; if if if... return res;
是假的,"res ="并不比"返回"好多少.它有单个return语句,但是函数实际结束的多个点.
如果你有多个返回函数(或"res ="s),通常最好将它分成几个带有单个退出点的较小函数.
我通常的策略是在函数末尾只有一个return语句,除非通过添加更多代码来大大减少代码的复杂性.事实上,我更喜欢Eiffel,它通过没有return语句强制执行唯一的返回规则(只有一个自动创建的'result'变量来放置你的结果).
当然有些情况下代码可以通过多次返回比没有它们的明显版本更清晰.有人可能会争辩说,如果你的函数太复杂而没有多个return语句就无法理解,那么就需要更多的返工,但有时候对这些事情务实是好的.
如果您最终得到的回报超过几个,那么您的代码可能会出现问题.否则我会同意,有时能够从子例程中的多个位置返回是很好的,特别是当它使代码更清晰时.
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);
}
}