Should a function have only one return statement?
为什么在一个函数中只有一个返回语句是一个更好的实践呢?
或者在逻辑正确的情况下从函数返回是否可以,这意味着函数中可能有许多返回语句?
我经常在一个方法的开头有几个语句来返回"简单"的情况。例如,这:
1 2 3 4 5 6 7 | public void DoStuff(Foo foo) { if (foo != null) { ... } } |
…可以像这样提高可读性(imho):
1 2 3 4 5 6 | public void DoStuff(Foo foo) { if (foo == null) return; ... } |
所以是的,我认为一个函数/方法有多个"出口点"是可以的。
没有人提到或引用完整的代码,所以我会这样做。
17.1返回最小化每个例程中的返回数。如果在底部阅读一个程序,你不知道它可能返回到上面的某个地方,就很难理解它。
当返回增强可读性时使用它。在某些例程中,一旦知道了答案,就希望立即将其返回到调用例程。如果例程的定义方式不需要任何清理,那么不立即返回意味着您必须编写更多的代码。
我会说,任意地决定反对多个退出点是非常不明智的,因为我发现这种技术在实践中一次又一次地很有用,事实上,为了清晰起见,我经常将现有代码重构为多个退出点。因此,我们可以比较这两种方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | 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; } |
将其与允许多个出口点的代码进行比较:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | 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; } |
我认为后者要清楚得多。据我所知,对多个退出点的批评是当今相当古老的观点。
我目前正在一个代码库中工作,其中两个工作人员盲目地订阅"单点退出"理论,我可以从经验中告诉你,这是一个可怕的实践。这使得代码极难维护,我将向您说明原因。
使用"单点退出"理论,您不可避免地会得到如下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | 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条件和大括号都正确匹配。
这种方法使得代码维护非常困难,并且容易出错。
结构化编程表示,每个函数只能有一个返回语句。这是为了限制复杂性。许多人如MartinFowler认为用多个返回语句编写函数更简单。他在他写的经典重构书中提出了这个论点。如果你遵循他的其他建议并编写一些小函数,这会很好地工作。我同意这一观点,并且只有严格的结构化编程纯粹主义者坚持每个函数只有一个返回语句。
正如KentBeck在讨论实现模式中的保护子句时指出的那样,使例程具有单一的入口和出口点…
"was to prevent the confusion possible
when jumping into and out of many
locations in the same routine. It made
good sense when applied to FORTRAN or
assembly language programs written
with lots of global data where even
understanding which statements were
executed was hard work ... with small methods and mostly local data, it is needlessly conservative."
我发现使用guard子句编写的函数比一长串嵌套的
在一个没有副作用的函数中,没有好的理由只返回一个值,您应该用函数样式来编写它们。在有副作用的方法中,事情的顺序性更强(时间索引),因此您使用命令式编写,使用RETURN语句作为停止执行的命令。
换句话说,如果可能的话,喜欢这种风格
1 2 3 | return a > 0 ? positively(a): negatively(a); |
在此之上
1 2 3 4 | if (a > 0) return positively(a); else return negatively(a); |
如果您发现自己编写了多个嵌套条件层,那么可能有一种方法可以重构它,例如使用谓词列表。如果您发现IFS和ELSE在语法上相距很远,那么您可能希望将其分解为较小的函数。一个跨越超过一屏文本的条件块是很难读取的。
没有适用于每种语言的硬性和快速性规则。类似于使用一个返回语句不会使代码良好。但好的代码往往会允许您以这种方式编写函数。
我在C++的编码标准中看到,这是一个C的挂起,好像你没有RAII或其他自动内存管理,那么你必须清理每一个返回,这意味着清理和粘贴清理或GOTO(逻辑上与"最终"在托管语言),这两者都被认为是坏的形式。如果您的实践是使用C++中的智能指针和集合或另一个自动存储系统,那么就没有一个很强的理由,它变成了可读性,更多的是一个判断调用。
我倾向于认为函数中间的返回语句是不好的。您可以使用RETURNS在函数的顶部构建一些保护子句,当然可以告诉编译器在函数的末尾返回什么,而无需发出任何问题,但是函数中间的返回很容易丢失,并且会使函数更难解释。
Are there good reasons why it's a better practice to have only one return statement in a function?
是的,有:
- 单一的退出点提供了一个很好的地方来断言您的岗位条件。
- 能够在函数末尾的一个返回上放置调试器断点通常很有用。
- 更少的回报意味着更少的复杂性。线性代码通常更容易理解。
- 如果试图将一个函数简化为一个返回值会导致复杂性,那么这就激励我们重构为更小、更通用、更易于理解的函数。
- 如果您使用的语言没有析构函数,或者不使用raii,那么一次返回可以减少必须清理的地方的数量。
- 有些语言需要一个单一的出口点(例如,pascal和eiffel)。
这个问题通常被认为是多个返回或深度嵌套的if语句之间的错误二分法。几乎总是有第三种解决方案是非常线性的(没有深嵌套),只有一个出口点。
更新:显然,米斯拉的指导方针也促进了单一退出。
很明显,我并不是说多次退货总是错误的。但是考虑到其他等价的解决方案,有很多很好的理由选择回报单一的方案。
拥有一个出口点确实在调试中提供了一个优势,因为它允许您在函数的末尾设置一个断点,以查看实际返回的值。
一般来说,我试图从一个函数中只有一个出口点。然而,有时这样做实际上会导致创建一个比需要的更复杂的函数体,在这种情况下,最好有多个出口点。它确实必须是基于结果复杂性的"判断调用",但是目标应该是尽可能少的退出点,而不牺牲复杂性和可理解性。
我倾向于单一出口,除非它真的使事情复杂化。我发现在某些情况下,多个存在点可以掩盖其他更重要的设计问题:
1 2 3 4 | public void DoStuff(Foo foo) { if (foo == null) return; } |
看到此代码后,我会立即问:
- "foo"是否为空?
- 如果是这样,有多少"dostuff"的客户机使用空的"foo"调用函数?
根据这些问题的答案,可能是
在上述两种情况下,可以使用断言对代码进行重写,以确保"foo"永远不为空,并且相关调用方发生了更改。
还有另外两个原因(具体是我认为C++代码),其中多个存在实际上会产生负面影响。它们是代码大小和编译器优化。
函数的出口中的作用域中的非POD C++对象将调用它的析构函数。如果有多个RETURN语句,则可能是范围中存在不同的对象,因此要调用的析构函数列表将不同。因此,编译器需要为每个返回语句生成代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 | 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' } |
如果代码大小是一个问题,那么这可能是值得避免的事情。
另一个问题涉及"命名返回值优化"(Akas Eclipse,ISO C++' 03 12.8/15)。C++允许一个实现跳过调用复制构造函数,如果它可以:
1 2 3 4 5 6 7 8 9 | A foo () { A a1; // do something return a1; } void bar () { A a2 ( foo() ); } |
只要将代码保持原样,对象"a1"在"foo"中构造,然后将调用其复制构造来构造"a2"。但是,复制省略允许编译器在堆栈上与"a2"相同的位置构造"a1"。因此,当函数返回时,不需要"复制"对象。
多个退出点使编译器在尝试检测这一点时的工作变得复杂,至少对于相对较新版本的VC++来说,优化没有发生在函数体有多个返回的地方。在Visual C++ 2005中查看命名返回值优化以获得更多细节。
不,因为我们不再生活在70年代了。如果函数足够长以致于多次返回是一个问题,那么它就太长了。
(除此之外,语言中的任何多行函数都会有多个出口点。)
我强迫自己只使用一条
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | 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; } |
(条件是仲裁…)
条件越多,函数越大,读取越困难。所以,如果你已经适应了代码的味道,你就会意识到它,并且想要重构代码。两种可能的解决方案是:
- 多次退货
- 重构为单独的函数
多次退货
1 2 3 4 5 6 | 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; } |
单独的功能
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | 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、Cype、Python或JavaScript等具有自动垃圾收集和EDCOX1×0个块的语言(以及EcOXX1中的1个块)在C语言中是不适用的。MMON需要集中手动资源分配。
在某些情况下,单个返回的可读性更强,而在某些情况下则不可读。请查看它是否减少了代码行数,使逻辑更清晰,或者减少了大括号、缩进或临时变量的数量。
因此,尽可能多地使用符合您的艺术敏感性的回报,因为它是一个布局和可读性问题,而不是一个技术问题。
我在我的博客上更详细地讨论过这个问题。
我会说,您应该有尽可能多的代码,或者任何使代码更干净的代码(如guard子句)。
我个人从未听过/见过任何"最佳实践"说您应该只有一个退货声明。
在大多数情况下,我倾向于根据逻辑路径尽快退出一个函数(保护子句就是一个很好的例子)。
关于有一个单一的出口点有很多好的事情要说,就像对于不可避免的"箭头"编程结果有很多坏的事情要说一样。
如果在输入验证或资源分配期间使用多个出口点,我会尝试将所有"错误出口"都非常明显地放在函数的顶部。
"ssslpedia"的斯巴达编程文章和"波特兰模式存储库的wiki"的单函数退出点文章对此都有一些深刻的争论。当然,也有这个帖子要考虑。
如果您真的想要一个单一的出口点(在任何非异常启用的语言中),例如为了在一个地方释放资源,我发现goto的谨慎应用是好的;例如,请参阅这个相当人为的示例(压缩以保存屏幕不动产):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | 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; } |
就我个人而言,总的来说,我更不喜欢箭头编程,而不是多个退出点,尽管两者在正确应用时都很有用。当然,最好的方法是将程序结构为既不需要也不需要。将您的函数分解为多个块通常有帮助:)
尽管如此,我还是发现我最终得到了多个出口点,就像在这个例子中,一些较大的函数被分解为几个较小的函数:
1 2 3 4 5 6 7 8 9 10 11 | 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语句(或者说,如果我不费吹灰之力就可以摆脱它的话),代码会更简单。非箭头。大多数非最终回报可能是例外。
简而言之;
- 少回报总比多回报好。
- 不止一次的返回比巨大的箭头更好,并且防护条款通常都可以。
- 在可能的情况下,异常可以/应该替换大多数"guard子句"。
我倾向于使用保护条款提前返回,否则在方法结束时退出。单输入和退出规则具有历史意义,特别是当处理遗留代码时,对于一个具有多个返回(和许多缺陷)的单个C++方法,运行到10个A4页是特别有用的。最近,公认的良好实践是保持方法小,这使得多个出口对理解的阻抗变小。在下面从上面复制的kronoz示例中,问题是//代码的其余部分中发生了什么…?:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | 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语句,然后该语句可以被视为一个guard子句。同样,在一个人为的例子中,代码的意图并不明显,somefunction()可能有一些其他的副作用,或者结果可以在//代码的其余部分中使用……
1 2 | if (string.IsNullOrEmpty(s) || i == null) return null; if (someFunction(s, i).Any(r => !r.Passed)) return null; |
提供以下重构函数:
1 2 3 4 5 6 7 8 9 | 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; } |
你知道俗语"美丽在旁观者眼中"。
有些人对netbeans发过誓,有些人对intellij的想法发过誓,有些人对python发过誓,有些人对php发过誓。
在某些商店,如果你坚持这样做,你可能会失去工作:
1 2 3 4 5 6 7 | public void hello() { if (....) { .... } } |
问题在于可见性和可维护性。
我沉迷于使用布尔代数来简化逻辑和使用状态机。然而,有一些以前的同事认为我在编码中使用"数学技术"是不合适的,因为它是不可见的和可维护的。那将是一种糟糕的做法。对不起,人们,我使用的技术对我来说是非常可见和可维护的-因为当我六个月后回到代码中时,我会清楚地理解代码,而不是看到一堆俗语意粉。
嘿,伙计(就像以前的客户常说的那样),做你想做的,只要你知道在我需要你修理它的时候如何修理它。
我记得20年前,我的一个同事因为采用了今天被称为敏捷开发策略而被解雇。他有一个细致的增量计划。但是他的经理对他大喊大叫:"你不能逐步向用户发布特性!他对经理的回应是,增量开发将更精确地满足客户的需求。他相信为客户需求开发,但经理相信编码是"客户需求"。
我们经常犯有破坏数据规范化、MVP和MVC边界的罪行。我们内联而不是构造一个函数。我们走捷径。
就我个人而言,我认为PHP是不好的实践,但我知道什么。所有的理论论据归根结底都是试图实现一套规则。
quality = precision, maintainability
and profitability.
所有其他规则都将淡入背景。当然,这条规则永远不会消失:
Laziness is the virtue of a good
programmer.
我能想到的一个很好的原因是代码维护:您只有一个出口点。如果您想更改结果的格式,…,实现起来就简单多了。另外,对于调试,您可以在那里插入一个断点:)
说了这句话,我曾经在一个图书馆工作,那里的编码标准规定"每个函数一个返回语句",我发现这相当困难。我写了很多数值计算代码,而且经常有"特殊情况",所以代码很难理解…
对于足够小的函数来说,多个出口点是很好的——也就是说,一个函数可以在一个屏幕上整体查看。如果一个冗长的函数同样包含多个出口点,则表示该函数可以进一步被切碎。
也就是说,除非绝对必要,否则我避免使用多个出口函数。我已经感觉到了bug带来的痛苦,这些bug是由于在更复杂的函数中某些模糊的行中出现了一些零散的返回。
我通常的策略是在一个函数的末尾只有一个返回语句,除非通过添加更多的语句大大降低了代码的复杂性。事实上,我更喜欢埃菲尔,它通过没有返回语句来强制执行唯一的返回规则(只有一个自动创建的"result"变量将结果放入其中)。
当然,在某些情况下,通过多次返回可以使代码更清晰,而没有它们的明显版本则更清晰。有人可能会说,如果一个函数太复杂,不需要多个返回语句就无法理解,那么就需要进行更多的重新编写,但有时候,在这种情况下保持务实是件好事。
单一的出口点——所有其他东西都一样——使得代码的可读性显著提高。但有一个陷阱:流行的建筑
1 2 3 | resulttype res; if if if... return res; |
是假的,"res="并不比"return"好多少。它有一个返回语句,但函数实际上在多个点结束。
如果函数具有多个返回值(或"res="s),通常最好将其分解为多个具有单个出口点的较小函数。
我使用了糟糕的编码标准,这些标准强制您使用单一的退出路径,结果几乎总是非结构化的意大利面条(如果函数不是很简单的话)——您最终会遇到很多中断,并继续执行这些中断。
我投票赞成在最后的单一回报作为指导方针。这有助于常见的代码清理处理…例如,看看下面的代码…
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | 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); } } |
如果返回的结果超过几个,则代码可能有问题。否则,我会同意,有时能够从子例程的多个位置返回是很好的,尤其是当它使代码更干净时。
Perl6:坏例子1 2 3 4 5 6 7 8 9 10 11 | 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 } } } |
最好这样写
Perl6:很好的例子1 2 3 4 5 6 7 8 9 10 11 12 13 | @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] } |
注意这只是一个简单的例子
如果可以只写下一个意见,那就是我的意见:
我完全和绝对不同意"单一返回语句理论",并且发现它在代码可读性、逻辑和描述方面大多是推测性的,甚至是破坏性的。
对于裸过程编程来说,只有一个返回的习惯甚至很差,更不用说高级抽象(函数、组合等)。此外,我希望用这种风格编写的所有代码都经过一些特殊的重写解析器,使其具有多个返回语句!
一个函数(如果它真的是一个函数/查询,根据"查询命令分离"的注释-参见eiffel编程语言。例如)只需要定义它所拥有的控制流场景的返回点。它更加清晰和数学上的一致;它是编写函数(即查询)的方法。
但我不会对你的经纪人收到的突变信息那么好战——程序调用。
函数中的返回语句越多,该方法的复杂性就越高。如果您发现自己想知道是否有太多的返回语句,那么您可能会想问自己在该函数中是否有太多的代码行。
但是,不是,一个/多个返回语句没有任何错误。在某些语言中,它是比其他语言(C)更好的实践。
这可能是一个不寻常的观点,但我认为,任何相信多个返回语句会受到青睐的人都不必在只支持4个硬件断点的微处理器上使用调试器。;-)
虽然"arrow code"的问题是完全正确的,但是在使用多个返回语句时,有一个问题似乎会消失,那就是您使用的是调试器。您没有方便的catch all位置来放置一个断点,以保证您将看到出口,从而看到返回条件。
唯一重要的问题是"代码如何更简单、可读性更好、更容易理解?"如果多次返回更简单,则使用它们。
拥有多个出口点与使用
总是需要一个返回类型是没有意义的。我认为这更多的是一个标志,一些事情可能需要简化。有时需要多次退货,但通常情况下,您可以通过至少尝试一个退出点来简化工作。
好吧,也许我是这里为数不多的几个足够大的人之一,能记起为什么"只有一个退货声明"如此艰难的原因之一。所以编译器可以发出更有效的代码。对于每个函数调用,编译器通常在堆栈上推送一些寄存器以保留它们的值。这样,函数就可以使用这些寄存器进行临时存储。当函数返回时,那些保存的寄存器必须从堆栈中弹出并返回到寄存器中。这是每个寄存器一个pop(或mov-(sp),rn)指令。如果您有一堆返回语句,那么要么每个语句都必须弹出所有寄存器(这会使编译后的代码变大),要么编译器必须跟踪哪些寄存器可能被修改,只弹出那些寄存器(减小代码大小,但增加编译时间)。
今天,坚持使用一个返回语句仍然有意义的一个原因是自动化重构的简单性。如果您的IDE支持方法提取重构(选择一系列行并将它们转换为一个方法),那么如果您要提取的行中有一个RETURN语句,特别是当您返回一个值时,就很难做到这一点。
由于错误处理,您已经隐式地拥有多个隐式返回语句,因此请处理它。
然而,正如编程中的典型情况一样,对于多重返回实践,也有支持和反对的例子。如果它能让代码更清晰,那么就这样或那样做。使用许多控制结构可以有所帮助(例如case语句)。
为了达到良好的标准和行业最佳实践,我们必须建立在所有职能部门中出现的正确数量的报表。很明显,大家一致反对发表一份退货声明。所以我建议我们把它定在两点。
如果现在每个人都能查看他们的代码,找到只有一个出口点的任何函数,并添加另一个出口点,我将不胜感激。在哪里不重要。
这种变化的结果无疑将是更少的错误,更大的可读性和难以想象的财富从天上掉到我们的头上。
我使用多个出口点,使错误案例+处理+返回值尽可能接近。
因此,必须测试条件a、b、c是否必须为真,并且您需要以不同的方式处理它们:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | if (a is false) { handle this situation (eg. report, log, message, etc.) return some-err-code } if (b is false) { handle this situation return other-err-code } if (c is false) { handle this situation return yet-another-err-code } perform any action assured that a, b and c are ok. |
A、B和C可能是不同的东西,比如A是输入参数检查,B是指向新分配内存的指针检查,C是检查"A"参数中的值。
有时出于性能方面的原因(我不想获取与continue相同需要的不同缓存线;有时)。
如果在不使用raii的情况下分配资源(内存、文件描述符、锁等),那么muliple返回可能容易出错,并且肯定是重复的,因为发布需要手动多次,因此必须仔细跟踪。
在例子中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | 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 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | function() { HRESULT error = OPERATION1FAILED;//assume failure if(SUCCEEDED(Operation1())) { error = OPERATION2FAILED;//assume failure if(SUCCEEDED(Operation3())) { error = OPERATION3FAILED;//assume failure if(SUCCEEDED(Operation3())) { error = OPERATION4FAILED; //assume failure if(SUCCEEDED(Operation4())) { error = S_OK; } } } } return error; } |
这当然更好。
这在手动资源发布的情况下特别有用,因为在哪里和哪些发布是必需的非常简单。如以下示例所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | function() { HRESULT error = OPERATION1FAILED;//assume failure if(SUCCEEDED(Operation1())) { //allocate resource for op2; char* const p2 = new char[1024]; error = OPERATION2FAILED;//assume failure if(SUCCEEDED(Operation2(p2))) { //allocate resource for op3; char* const p3 = new char[1024]; error = OPERATION3FAILED;//assume failure if(SUCCEEDED(Operation3(p3))) { error = OPERATION4FAILED; //assume failure if(SUCCEEDED(Operation4(p2,p3))) { error = S_OK; } } //free resource for op3; delete [] p3; } //free resource for op2; delete [] p2; } return error; } |
如果编写此代码时没有RAII(忘记了异常的问题!)如果存在多个出口,则必须多次写入删除。如果你用
但RAII使多出口资源问题变得没有意义。
我总是避免使用多个返回语句。即使是在小功能中。小函数可以变得更大,跟踪多个返回路径会使跟踪正在发生的事情变得更困难(在我的小头脑中)。一次返回也使调试更容易。我见过有人发帖说,除了多个RETURN语句之外,唯一的选择是一个嵌套的if语句的杂乱箭头10级深。虽然我确信这样的编码确实发生了,但这并不是唯一的选择。我不会在多个返回语句和一个ifs嵌套之间做出选择,我会对其进行重构,这样您就可以同时删除这两个语句。这就是我编码的方式。下面的代码消除了这两个问题,在我看来,非常容易阅读:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | public string GetResult() { string rv = null; bool okay = false; okay = PerformTest(1); if (okay) { okay = PerformTest(2); } if (okay) { okay = PerformTest(3); } if (okay) { okay = PerformTest(4); }; if (okay) { okay = PerformTest(5); } if (okay) { rv ="All Tests Passed"; } return rv; } |
我更喜欢单一的退货声明。尚未指出的一个原因是,对于单个退出点(例如EclipseJDT extract/inline方法),一些重构工具工作得更好。
您可以这样做,只实现一个返回语句-在开始时声明它,在结束时输出它-问题已解决:
1 2 3 4 5 6 7 8 9 10 11 12 13 | $content =""; $return = false; if($content !="") { $return = true; } else { $return = false; } return $return; |
作为嵌套IFS的替代方案,有一种方法可以使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | function() { HRESULT error = S_OK; do { if(!SUCCEEDED(Operation1())) { error = OPERATION1FAILED; break; } if(!SUCCEEDED(Operation2())) { error = OPERATION2FAILED; break; } if(!SUCCEEDED(Operation3())) { error = OPERATION3FAILED; break; } if(!SUCCEEDED(Operation4())) { error = OPERATION4FAILED; break; } } while (false); return error; } |
这就为您提供了一个出口点,允许您进行其他嵌套操作,但仍然不是真正的深层结构。如果你不喜欢的话!成功你可以做任何失败的事。这种方式还允许您在任何两个其他检查之间添加其他代码,而无需重新缩进任何内容。
如果你真的疯了,那么整个
1 2 3 4 5 6 7 8 9 | #define BREAKIFFAILED(x,y) if (!SUCCEEDED((x))) { error = (Y); break; } do { BREAKIFFAILED(Operation1(), OPERATION1FAILED) BREAKIFFAILED(Operation2(), OPERATION2FAILED) BREAKIFFAILED(Operation3(), OPERATION3FAILED) BREAKIFFAILED(Operation4(), OPERATION4FAILED) } while (false); |
我认为在不同的情况下,不同的方法更好。例如,如果您应该在返回之前处理返回值,那么应该有一个退出点。但在其他情况下,使用多个收益更为舒适。
一个音符。如果在几种情况下(而不是全部情况下)应在返回之前处理返回值,则最好的解决方案(imho)定义一个方法(如processval)并在返回之前调用它:
1 2 3 4 5 6 7 | var retVal = new RetVal(); if(!someCondition) return ProcessVal(retVal); if(!anotherCondition) return retVal; |
我可能会为此感到讨厌,但理想情况下,我认为根本不应该有返回语句,一个函数应该只返回它的最后一个表达式,并且在完全理想的情况下应该只包含一个表达式。
所以不
1 2 3 4 5 6 | function name(arg) { if (arg.failure?) return; //code for non failure } |
而是
1 2 3 4 5 6 7 8 | function name(arg) { if (arg.failure?) voidConstant else { //code for non failure } |
如果不是表达式的语句和返回语句对我来说是一种非常可疑的做法。
有人可能会说…如果在执行函数的任务之前必须满足多个条件,则在满足这些条件之前,不要调用函数:
而不是:
1 2 3 | function doStuff(foo) { if (foo != null) return; } |
或
1 2 3 4 5 | function doStuff(foo) { if (foo !== null) { ... } } |
在foo之前不要调用
1 | if(foo != null) doStuff(foo); |
这就要求每个调用站点确保在调用之前满足调用的条件。如果有多个调用站点,那么这个逻辑最好放在单独的函数、待调用函数的方法(假设它们是第一类公民)或代理中。
关于函数是否在数学上可证明的问题,考虑语法上的逻辑。如果一个函数有多个返回点,这并不意味着(默认情况下)它在数学上是不可证明的。
这主要是来自Fortran的一个挂起,在那里可以将多个语句标签传递给一个函数,这样它就可以返回到其中的任何一个。
所以这种代码是完全有效的
1 2 3 4 5 6 7 8 | CALL SOMESUB(ARG1, 101, 102, 103) C Some code 101 CONTINUE C Some more code 102 CONTINUE C Yet more code 103 CONTINUE C You get the general idea |
但是被调用的函数决定了代码路径的去向。效率高?可能。可维护的?不。
这就是规则的来源(顺便说一下,一个函数没有多个入口点,这在Fortran和汇编程序中是可能的,但在C中不是可能的)。
但是,它的措辞看起来像可以应用到其他语言(关于多个入口点的语言不能应用到其他语言,因此它实际上不是一个程序)。所以这个规则被延续了下来,即使它涉及到一个完全不同的问题,也不适用。
对于更结构化的语言,需要删除该规则,或者至少仔细考虑一下。当然,一个函数中有大量的返回是很难理解的,但是在开始时返回并不是一个问题。在某些C++编译器中,如果只从一个地方返回值,单个返回点可以生成更好的代码。
但原来的规则被误解了,误用了。不再相关。
我通常支持多个返回语句。它们是最容易阅读的。
有些情况下情况不好。有时从函数返回可能非常复杂。我记得一个例子,所有函数都必须链接到多个不同的库中。一个库期望返回值是错误/状态代码,而其他库则没有。使用一个返回语句可以节省时间。
我很惊讶没有人提到Goto。Goto并不是所有人都会相信的编程的祸根。如果每个函数中必须只有一个返回,请将其放在末尾,并根据需要使用goto跳转到该返回语句。一定要避免使用既难看又运行缓慢的标志和箭头编程。
不应在方法中使用RETURN语句。
我知道我会为此兴奋不已,但我是认真的。好的。
返回语句基本上是过程编程时代的遗留问题。它们是goto的一种形式,以及break、continue、if、switch/case,而for、yield和其他一些语句以及大多数现代编程语言中的等价物。好的。
返回语句有效地"转到"调用函数的位置,在该范围内分配一个变量。好的。
返回语句就是我所说的"方便的噩梦"。他们似乎做得很快,但造成了大量的维修头痛。好的。返回语句与封装完全相反
这是面向对象编程最重要和最基本的概念。这是OOP的原因。好的。
每当您从一个方法中返回任何东西时,基本上都是从对象中"泄漏"状态信息。不管您的状态是否发生了变化,也不管这些信息是否来自其他对象——这对调用者没有任何影响。这样做的目的是允许对象的行为在破坏对象的封装之外。它允许调用者以导致脆弱设计的方式开始操作对象。好的。洛德是你的朋友
我建议任何开发者在c2.com或维基百科上阅读有关德米特定律(lod)的内容。LOD是一种设计理念,它被用于在文字意义上具有真正的"关键任务"软件约束的地方,比如JPL。已经证明它可以减少代码中的错误数量并提高灵活性。好的。
有一个很好的类比是基于遛狗。当你遛狗的时候,你身体上不会抓住它的腿,移动它们让狗走路。你命令狗走路,它照顾自己的腿。在这个类比中,返回语句相当于狗让你抓住它的腿。好的。只与你的直接朋友交谈:
您会注意到这些都不需要返回语句。您可能认为构造函数是一个返回,并且您正在进行某种操作。实际上,返回来自内存分配器。构造函数只设置内存中的内容。只要这个新对象的封装是正常的,这就可以了,因为在您创建它的时候,您对它有完全的控制权——没有其他人可以破坏它。好的。
访问其他对象的属性是正确的。盖特人出局了(但你知道他们已经很糟糕了,对吧?).setter可以,但最好使用构造函数。继承是不好的-当您从另一个类继承时,该类中的任何更改都可能并且可能会破坏您。类型嗅探是坏的(是的)LoD暗示Java/C++风格类型的调度是不正确的-询问类型,甚至隐含地,正在破坏封装。类型是对象的隐式属性。接口是正确的)。好的。
为什么这都是个问题?好吧,除非你的世界和我的大不相同,否则你要花很多时间调试代码。你写的代码不是你计划永远不会重用的。您的软件需求正在变化,这会导致内部API/接口发生变化。每次使用返回语句时,都会引入一个非常复杂的依赖关系——返回任何内容的方法都需要知道它们返回的内容将如何使用——这就是每种情况!一旦界面发生变化,在一端或另一端,一切都可能中断,您将面临漫长而乏味的bug搜寻。好的。
在您的代码中,它们确实是一种恶性癌症,因为一旦您开始使用它们,它们就会促进在其他地方的进一步使用(这就是为什么您经常可以在对象系统中找到返回的方法链)。好的。
那么还有什么选择呢?好的。说吧,别问。
使用OOP-目标是告诉其他对象要做什么,并让他们来处理它。所以你必须忘记做事情的程序方法。这真的很简单-永远不要写返回语句。有很多更好的方法来做同样的事情:好的。退货概念没有什么错,但是退货声明有很大的缺陷。
如果你真的需要回电-使用回电。传入要填充的数据结构,偶数。这样,您就可以保持界面的整洁和开放性以进行更改,并且您的整个系统也不那么脆弱,更具适应性。它不会减慢系统的速度,事实上,它可以像尾调用优化一样加快系统的速度-除非在这种情况下,没有尾调用,所以您甚至不必浪费时间用返回值来操作堆栈。好的。
如果遵循这些参数,您将发现实际上不需要返回语句。好的。
如果您遵循这些实践,我保证很快您就会发现,您花费的时间更少,查找bug的时间更少,更快速地适应需求更改,理解您自己的代码的问题也更少。好的。好啊。
如果你管理好的话,多出口是很好的。
第一步是指定退出的原因。我的通常是这样的:1。不需要执行函数2。发现错误三。提前完工4。正常完成我想你可以把"1"分组。无需执行"into"3功能。提前完成"(如果你愿意的话,很早完成)。
第二步是让外界知道这个功能退出的原因。伪代码如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | function foo (input, output, exit_status) exit_status == UNDEFINED if (check_the_need_to_execute == false) then exit_status = NO_NEED_TO_EXECUTE // reason #1 exit useful_work if (error_is_found == true) then exit_status = ERROR // reason #2 exit if (need_to_go_further == false) then exit_status = EARLY_COMPLETION // reason #3 exit more_work if (error_is_found == true) then exit_status = ERROR else exit_status = NORMAL_COMPLETION // reason #4 end function |
显然,如果将上图中的工作块移动到单独的函数中是有益的,那么应该这样做。
如果您愿意,您可以更具体地了解退出状态,例如,使用几个错误代码和早期完成代码来确定退出的原因(甚至位置)。
即使您强制这个函数进入只有一个出口的函数,我认为您仍然需要指定出口状态。调用者需要知道是否可以使用输出,这有助于维护。