是否有充分的理由说明为什么在函数中仅包含一个return语句是一种更好的做法?
还是在逻辑上正确地从函数中返回就可以,这意味着函数中可能有很多return语句?
是否有充分的理由说明为什么在函数中仅包含一个return语句是一种更好的做法?
还是在逻辑上正确地从函数中返回就可以,这意味着函数中可能有很多return语句?
Answers:
在方法开始时,我经常会有几条陈述返回“轻松”情况。例如,这:
public void DoStuff(Foo foo)
{
if (foo != null)
{
...
}
}
...可以像这样变得更具可读性(IMHO):
public void DoStuff(Foo foo)
{
if (foo == null) return;
...
}
因此,是的,我认为从一个函数/方法中获得多个“退出点”很好。
DoStuff() { DoStuffInner(); IncreaseStuffCallCounter(); }
没有人提及或引用“代码完成”,所以我会做。
最小化每个例程中的返回数。如果在底部阅读该例程,而又没有意识到该例程可能会返回到上方,则很难理解该例程。
在提高可读性时使用return。在某些例程中,一旦知道答案,就想立即将其返回给调用例程。如果例程的定义不需要任何清理,则不立即返回就意味着您必须编写更多代码。
我会说,针对多个出口点进行任意决定是非常不明智的做法,因为我发现该技术在实践中一遍又一遍有用,实际上,为了清楚起见,我经常将现有代码重构为多个出口点。因此,我们可以比较两种方法:
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之间添加一个操作。您必须缩进整个freaking函数,并祝您好运,确保所有您的if / else条件和括号正确匹配。
这种方法使代码维护极为困难且容易出错。
结构化编程说每个函数只能有一个return语句。这是为了限制复杂性。许多人(例如Martin Fowler)认为,使用多个return语句编写函数更简单。他在他撰写的经典重构书中提出了这一论点。如果您遵循他的其他建议并编写了一些小函数,则此方法效果很好。我同意这种观点,并且只有严格的结构化编程纯粹主义者遵守每个函数的单个返回语句。
GOTO
即使在功能存在时也使用移动控制流。它从不说“从不使用GOTO
”。
正如肯特·贝克(Kent Beck)在讨论实现模式中的保护条款时所指出的,使例程具有单个入口和出口点...
“这是为了防止在同一例程中跳入和跳出许多位置时可能造成的混乱。将其应用于用大量全局数据编写的FORTRAN或汇编语言程序时,即使了解执行了哪些语句也很困难,这是很有意义的。 ……使用小的方法和大部分本地数据,就显得过于保守了。”
我发现用一个保护子句编写的函数比一堆长嵌套的if then else
语句容易遵循。
在没有副作用的函数中,没有充分的理由要有一个以上的收益,而应该以函数样式编写它们。在具有副作用的方法中,事情更具顺序性(按时间索引),因此您以命令式的方式编写,将return语句用作停止执行的命令。
换句话说,请尽可能支持这种样式
return a > 0 ?
positively(a):
negatively(a);
在这个
if (a > 0)
return positively(a);
else
return negatively(a);
如果您发现自己编写了多层嵌套条件,则可能有一种方法可以重构它,例如使用谓词列表。如果您发现if和else在语法上相距甚远,则可能需要将其分解为较小的函数。跨越多个屏幕文本的条件块很难阅读。
没有适用于每种语言的严格规则。诸如拥有单个return语句之类的东西不会使您的代码良好。但是好的代码将倾向于允许您以这种方式编写函数。
我已经在C ++的编码标准中看到了这一点,它是C的遗留物,就好像您没有RAII或其他自动内存管理一样,因此您必须为每次返回清理一下,这意味着剪切和粘贴清理或转到(逻辑上与托管语言中的“最终”相同),这两种形式均被视为错误形式。如果您的做法是在C ++或其他自动内存系统中使用智能指针和集合,则没有充分的理由,这全都与可读性有关,更多地是判断力。
auto_ptr
可以并行使用普通指针。尽管首先使用非优化编译器编写“优化”代码是很奇怪的。
try
... finally
),并且您需要执行资源维护,则只需一个即可在方法末尾返回。在执行此操作之前,您应该认真考虑重构代码以摆脱这种情况。
我倾向于认为函数中间的return语句是不好的。您可以使用return在函数顶部构建一些保护子句,并且当然告诉编译器在函数末尾没有问题的情况下返回什么,但是在中间返回函数的可能很容易遗漏并且可以使函数难以解释。
是否有充分的理由说明为什么在函数中仅包含一个return语句是一种更好的做法?
是,有:
这个问题经常被认为是多次返回之间的错误二分法,或者是嵌套在if语句中的嵌套。几乎总是有第三个解决方案,它只有一个出口点就非常线性(没有深层嵌套)。
更新:显然,MISRA准则也促进了单一出口。
明确地说,我并不是说拥有多个回报总是错误的。但是,如果给出了其他等效的解决方案,则有很多充分的理由偏爱单收益的解决方案。
Contract.Ensures
多个返回点。
goto
常见的清理代码,则可能已经简化了该函数,因此清理代码return
的末尾只有一个。因此,您可以说您已经解决了问题goto
,但我想通过简化为一个解决了问题return
。
具有单个出口点确实在调试中提供了一个优势,因为它使您可以在函数的末尾设置单个断点,以查看实际将返回的值。
通常,我尝试从功能中仅获得一个出口。但是,有时这样做实际上最终会创建一个比必要的复杂的函数体,在这种情况下,最好有多个出口点。实际上,这必须是基于结果复杂性的“判断调用”,但是目标应该是在不牺牲复杂性和可理解性的情况下尽可能减少出口。
不,因为我们不再生活在1970年代。如果您的函数足够长而导致多次返回是一个问题,那就太长了。
(除了以下事实外,任何语言中的任何多行函数(带有例外)都将有多个出口点。)
我的选择是单次退出,除非它确实使事情复杂化。我发现在某些情况下,多个存在点可以掩盖其他更重要的设计问题:
public void DoStuff(Foo foo)
{
if (foo == null) return;
}
看到此代码后,我将立即询问:
根据这些问题的答案,可能是
在以上两种情况下,都可以使用断言重新编写代码,以确保'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中的命名返回值优化。
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代码中是一个坏习惯,在C代码中必须显式地取消分配资源,但是Java,C#,Python或JavaScript之类的语言具有自动垃圾收集和try..finally
块(以及using
C#中的块)构造),并且该参数不适用-在这些语言中,需要集中手动分配资源非常罕见。
在某些情况下,单项退货更具可读性,而在某些情况下则不易理解。看看它是否减少了代码行数,使逻辑更清晰或减少了花括号,缩进或临时变量的数量。
因此,使用尽可能多的退货来满足您的艺术敏感性,因为这是布局和可读性问题,而不是技术问题。
关于具有单个出口点,有很多好话要说,就像对不可避免的“箭头”编程有不好的话要说。
如果在输入验证或资源分配过程中使用多个出口点,我会尝试将所有“错误出口”放在功能顶部。
关于“ 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语句(或者根本不会使用足够少的精力就可以摆脱它),并且代码会更简单。和非箭头。而且大多数非最终回报可能都是例外。
简而言之;
你知道格言- 情人在旁观者眼中。
有些人发誓的NetBeans和一些由IntelliJ IDEA的,一些由Python的一些由PHP。
如果坚持这样做,在某些商店中,您可能会失业:
public void hello()
{
if (....)
{
....
}
}
问题全在于可见性和可维护性。
我沉迷于使用布尔代数来简化和简化逻辑以及使用状态机。但是,以前的同事们认为我在编码中使用“数学技术”是不合适的,因为它不可见且不可维护。那将是一个坏习惯。抱歉,我使用的技术对我来说是非常明显和可维护的-因为六个月后当我返回代码时,我会清楚地理解代码,而不会看到一团混乱的意大利面条。
嘿哥们(就像以前的一位前客户所说的)会做您想做的事情,只要您知道如何解决它,当我需要您修复它时。
我记得20年前,我的一位同事因采用今天称为敏捷开发策略的工作而被解雇。他有一个细致的增量计划。但是他的经理对他大吼:“您不能向用户逐步发布功能!您必须坚持瀑布式设计。” 他对经理的回应是,渐进式开发将更精确地满足客户的需求。他相信要开发满足客户需求的产品,但经理相信要按照“客户要求”进行编码。
我们经常因违反数据规范化,MVP和MVC界限而感到内gui。我们内联而不是构造一个函数。我们采取捷径。
我个人认为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;
}
null
而不是抛出一个异常,指出该参数不被接受呢?
我可以想到的一个很好的理由是代码维护:您只有一个出口。如果您想更改结果的格式,...,它的实现要简单得多。另外,为了进行调试,您可以在此处放置一个断点:)
话虽如此,我曾经不得不在一个编码标准强加“每个函数一个返回语句”的库中工作,我发现这非常困难。我写了很多数值计算代码,并且经常有“特殊情况”,所以代码最终很难遵循...
我曾使用过糟糕的编码标准,这些标准强制您使用一个退出路径,并且如果该功能除琐碎的事情之外,结果几乎总是非结构化的意大利面-您最终会遇到很多麻烦,并继续遇到麻烦。
if
每个返回成功与否的方法调用前语句:(
我通常的策略是在一个函数的末尾只有一个return语句,除非通过添加更多代码来大大降低代码的复杂性。实际上,我是Eiffel的粉丝,它通过没有return语句(只有自动创建的“结果”变量来放入结果)来强制执行唯一的返回规则。
当然,在某些情况下,可以使多次返回的代码比没有它们的明显版本更清晰。有人可能会争辩说,如果您的函数过于复杂而无法在没有多个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]
}
请注意,这只是一个简单的例子
最后,我投票给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);
}
}
这可能是一个不同寻常的观点,但是我认为,任何认为应该支持多个return语句的人都不必在仅支持4个硬件断点的微处理器上使用调试器。;-)
尽管“箭头代码”的问题是完全正确的,但是在使用多个返回语句时似乎已经消失的一个问题是使用调试器的情况。您没有方便的通用位置放置断点以确保您将看到出口以及返回条件。