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

是否有充分的理由说明在函数中只有一个返回语句是更好的做法?

还是可以从函数返回,只要它在逻辑上是正确的,这意味着函数中可能有许多返回语句?

343038 次浏览

我会说你应该有尽可能多的需要,或任何使代码更清晰(如保护条款)。

我个人从未听过/见过任何“最佳实践”说你应该只有一个返回声明。

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

我能想到的一个很好的理由是用于代码维护:你有一个单点退出。如果你想更改结果的格式,…,实现起来要简单得多。此外,对于调试,你可以在那里贴一个断点:)

话虽如此,但我有一次不得不在一个库工作,那里的编码标准强制要求每个函数一个返回语句,我发现这相当困难。我写了很多数值计算代码,经常有特殊情况,所以代码最终很难理解…

我经常在方法的开头有几个语句来返回“简单”的情况。例如,这:

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

…可以变得更可读(IMHO)像这样:

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

所以是的,我认为从一个函数/方法中有多个“退出点”是可以的。

结构化编程说每个函数应该只有一个返回语句。这是为了限制复杂性。许多人,如Martin Fowler,认为用多个返回语句编写函数更简单。他在他写的经典重构书中提出了这个论点。如果你遵循他的其他建议并编写小函数,这很有效。我同意这个观点,只有严格的结构化编程纯粹主义者坚持每个函数只有一个返回语句。

我曾经使用过可怕的编码标准,这些标准强制你使用单一的退出路径,如果函数不是微不足道的,结果几乎总是非结构化的意大利面条——你最终会有很多中断并继续下去,这只会妨碍你。

我想说,任意决定多个退出点是非常不明智的,因为我发现该技术在实践中很有用一遍又一遍,事实上,为了清晰起见,我经常重构现有代码到多个退出点。我们可以这样比较这两种方法:-

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

我认为后者要清晰得多。据我所知,对多个退出点的批评是当今相当过时的观点。

我在C遗留下来的C++的编码标准中看到过,好像你没有RAII或其他自动内存管理,那么你必须对每次返回进行清理,这要么意味着清理的剪切和粘贴,要么是goto(逻辑上与托管语言中的“最终”相同),这两种都被认为是糟糕的形式。如果你的做法是在C++或其他自动内存系统中使用智能指针和集合,那么它就变得非常易读性,更多的是判断性的调用。

正如Kent Beck在讨论实施模式中的保护条款时所指出的,使例程具有单个入口和出口点…

“是为了防止可能的混乱当跳进跳出许多都在同一个地方。这让适用于FORTRAN或编写的汇编语言程序有大量的全球数据,甚至了解哪些陈述是“执行是一项艰苦的工作……使用小方法和大部分本地数据,它是不必要的保守。

我发现一个用Guard子句编写的函数比一个长嵌套的if then else语句更容易理解。

一般来说,我尝试一个函数只有一个出口点。然而,有时这样做实际上最终会创建一个比必要的更复杂的函数体,在这种情况下,最好有多个出口点。这确实必须是基于结果复杂性的“判断调用”,但目标应该是在不牺牲复杂性和可理解性的情况下,尽可能少的出口点。

我目前正在研究一个代码库,其中两个人盲目地订阅“单点退出”理论,我可以告诉你,从经验来看,这是一种可怕的做法。它使代码非常难以维护,我会告诉你为什么。

使用“单点退出”理论,您不可避免地会得到如下代码:

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条件和大括号都正确匹配。

这种方法使得代码维护非常困难并且容易出错。

对于足够小的函数来说,多个退出点是很好的——也就是说,一个函数可以在一个屏幕长度上完整地查看。如果一个冗长的函数同样包括多个退出点,这表明该函数可以进一步切分。

也就是说,我避免了多退出函数除非绝对必要。我感受到了错误的痛苦,这些错误是由于在更复杂的函数中的一些模糊行中的一些杂散返回。

有一个单一的出口点有好的事情要说,就像有不好的事情要说不可避免的"箭头"编程结果。

如果在输入验证或资源分配期间使用多个退出点,我尝试将所有“错误退出”非常明显地放在函数的顶部。

“SSDSLPedia”的第0篇文章和“Portland模式存储库的Wiki”的第1篇文章都围绕这一点提出了一些有见地的论点。当然,还有这篇文章需要考虑。

如果你真的想要一个出口点(在任何非异常启用的语言中),例如为了在一个地方释放资源,我发现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语句(或者根本不会,如果我能毫不费力地摆脱它),代码会更加简单。而且是非箭头的。而且大多数非最终返回可能是异常。

简而言之;

  • 很少的回报比很多回报更好
  • 不止一个返回比巨大的箭头更好,保护条款通常是可以的。
  • 在可能的情况下,例外可以/应该取代大多数“保护条款”。

只有一个退出点减少了圈复杂性,因此理论上减少了在更改代码时将错误引入代码的可能性。然而,实践往往表明需要一种更务实的方法。因此,我倾向于有一个单一的退出点,但如果更具可读性,请允许我的代码有几个。

我通常赞成多个返回语句。它们最容易阅读。

有些情况是不好的。有时从函数返回可能非常复杂。我记得有一个案例,所有函数都必须链接到多个不同的库中。一个库理论收益值是错误/状态代码,而其他库则不是。有一个返回语句可以节省时间。

我很惊讶没有人提到goto。goto并不是编程的祸根,每个人都希望你相信。如果你必须在每个函数中只有一个返回值,把它放在最后,并根据需要使用gotos跳转到该返回语句。绝对避免标志和箭头编程,它们既丑陋又运行缓慢。

在没有副作用的函数中,没有充分的理由有多个返回值,你应该用函数式风格编写它们。在有副作用的方法中,事情更具顺序性(时间索引),所以你用命令式风格编写,使用返回语句作为停止执行的命令。

换句话说,如果可能的话,喜欢这种风格

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

对这

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

如果你发现自己编写了多层嵌套条件,可能有一种方法可以重构它,例如使用谓词列表。如果你发现你的ifs和elses在语法上相距甚远,你可能想把它分解成更小的函数。跨越超过一屏幕文本的条件块很难阅读。

没有适用于每种语言的硬性规则。类似于只有一个返回语句不会让你的代码变得好。但好的代码往往允许你以这种方式编写函数。

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

但是,不是,一个/多个返回语句没有问题。在某些语言中,这是比其他语言(C)更好的实践(C++)。

您已经隐式拥有多个由错误处理引起的隐式返回语句,因此请处理它。

然而,就像编程的典型情况一样,有支持和反对多重返回实践的例子。如果它使代码更清晰,请这样做或那样做。使用许多控制结构可以有所帮助(例如案件语句)。

单一出口点-所有其他条件相同-使代码更具可读性。但有一个陷阱:流行的建筑

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

是假的,“res=”并不比“back”好多少。它有一个返回语句,但函数实际结束的多点。

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

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

肯定有一些情况下,使用多个返回语句可以让代码比没有它们的明显版本更清晰。有人可能会说,如果你的函数太复杂,没有多个返回语句就无法理解,那么需要更多的返工。但有时对这类事情保持务实是件好事。

我使用多个退出点,以使错误案例+处理+返回值尽可能接近。

因此,必须测试条件a,b,c必须为真,并且您需要以不同的方式处理它们:

if (a is false) {handle this situation (eg. report, log, message, etc.)return some-err-code}if (b is false) {handle this situationreturn other-err-code}if (c is false) {handle this situationreturn yet-another-err-code}
perform any action assured that a, b and c are ok.

a、b和c可能是不同的东西,例如a是输入参数检查,b是指向新分配内存的指针检查,c是检查'a'参数中的值。

如果你最终得到了几个以上的返回,那么你的代码可能有问题。否则,我同意有时能够从子例程中的多个地方返回是很好的,尤其是当它使代码更清晰时。

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{zeroonetwothreefour...}sub Int_to_String( Int i ){return undef if i < 0;return undef unless i < @Int_to_String.length;return @Int_to_String[i]}

注意这只是一个简单的例子

我倾向于单出口,除非它真的使事情复杂化。我发现在某些情况下,多个存在点可以掩盖其他更重要的设计问题:

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

看到这段代码,我会立即问:

  • foo是否为空?
  • 如果是这样,有多少'DoStuff'的客户端使用空'foo'调用函数?

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

  1. 检查是毫无意义的,因为它永远不会是true(即。它应该是一个断言)
  2. 检查很少为真,因此更改这些特定的调用者函数可能更好,因为它们可能无论如何都应该采取一些其他操作。

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

还有另外两个原因(我认为具体到C++代码),其中多个存在实际上可能会产生影响。它们是代码大小和编译器优化。

函数出口处作用域中的非PODC++对象将调用其析构函数。如果有几个返回语句,可能是作用域中有不同的对象,因此要调用的析构函数列表将不同。因此编译器需要为每个返回语句生成代码:

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

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

另一个问题与“命名返回值优化”(又名复制限定,ISOC++'03 12.8/15)有关。C++允许实现跳过调用复制构造函数,如果可以:

A foo () {A a1;// do somethingreturn a1;}
void bar () {A a2 ( foo() );}

只要按原样获取代码,对象'a1'在'foo'中构造,然后调用其复制构造来构造'a2'。然而,复制省略允许编译器在堆栈上与'a2'相同的位置构造'a1'。因此,当函数返回时,无需“复制”对象。

多个退出点使编译器试图检测到这一点的工作变得复杂,至少对于一个相对较新的VC++版本,优化没有发生在函数体有多个返回的地方。有关更多详细信息,请参阅VisualC++2005中的命名返回值优化

多出口是好的,如果你管理得好

第一步是指定退出的原因。我的通常是这样的:
1.无需执行函数
2.找到错误
3.早日完成
4.正常完成
我想你可以将“1.不需要执行函数”分组到“3.早期完成”(如果你愿意的话,这是一个非常早期的完成)。

第二步是让函数之外的世界知道退出的原因。伪代码看起来像这样:

function foo (input, output, exit_status)
exit_status == UNDEFINEDif (check_the_need_to_execute == false) thenexit_status = NO_NEED_TO_EXECUTE  // reason #1exit
useful_work
if (error_is_found == true) thenexit_status = ERROR               // reason #2exitif (need_to_go_further == false) thenexit_status = EARLY_COMPLETION    // reason #3exit
more_work
if (error_is_found == true) thenexit_status = ERRORelseexit_status = NORMAL_COMPLETION   // reason #4
end function

显然,如果将上图中的大量工作移动到一个单独的函数中是有益的,那么您应该这样做。

如果您愿意,您可以更具体地了解退出状态,例如,使用多个错误代码和早期完成代码来确定退出的原因(甚至位置)。

即使你强制这个函数进入一个只有一个出口的函数,我认为你仍然需要指定出口状态。调用者需要知道使用输出是否可以,这有助于维护。

作为嵌套IF的替代方案,有一种方法可以使用do/while(false)在任何地方爆发:

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

这会给你一个退出点,让你有其他嵌套操作,但仍然不是一个真正的深层结构。如果你不喜欢!成功,你总是可以做失败的任何事情。这种事情还允许你在任何两个其他检查之间添加其他代码,而无需重新缩进任何东西。

如果你真的疯了,整个if块也可以被宏化。: D

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

这可能是一个不寻常的观点,但我认为任何相信多个返回语句会受到青睐的人都不必在仅支持4个硬件断点的微处理器上使用调试器。;-)

虽然“箭头代码”的问题是完全正确的,但在使用多个返回语句时,有一个问题似乎消失了,那就是您正在使用调试器。您没有方便的包罗万象的位置来放置断点来保证您将看到退出并因此看到返回条件。

总是要求一个单一的返回类型是没有意义的。我认为这更像是一个标志,可能需要简化一些东西。有时有多个返回是必要的,但通常你可以通过至少试图来简化事情,以拥有一个退出点。

您应该从未在方法中使用返回语句。

我知道我会被责备,但我是认真的。

返回语句基本上是过程式编程时代遗留下来的,它们是goto的一种形式,在大多数现代编程语言中,它还包含了关掉、继续、如果、开关/案例、同时、for、产量和其他一些语句以及等价物。

返回语句有效地“GOTO”调用函数的点,在该范围内分配一个变量。

返回语句就是我所说的“方便的噩梦”。它们似乎可以快速完成工作,但会导致大量的维护问题。

返回语句与封装截然相反

这是面向对象程序设计最重要和最基本的概念,也是OOP存在的理由。

每当你从一个方法返回任何东西时,你基本上都是从对象中“泄漏”状态信息。你的状态是否发生了变化并不重要,也不重要这些信息是否来自其他对象——它对调用者没有影响。这样做是允许对象的行为在对象之外——破坏封装。它允许调用者开始以导致脆弱设计的方式操纵对象。

LoD是你的朋友

我建议任何开发人员在c2.com或维基百科上阅读LoD原则(LoD)。LoD是一种设计理念,已被用于具有真正的“关键任务”软件约束的地方,如JPL。它已被证明可以减少代码中的错误数量并提高灵活性。

在遛狗的基础上有一个很好的类比。当你遛狗的时候,你不是用身体抓住它的腿,然后移动它们让狗走路。你命令狗走路,它照顾自己的腿。这个类比中的返回语句相当于狗让你抓住它的腿。

只与你的直接朋友交谈:

  1. 您所在函数的参数,
  2. 自己的属性,
  3. 您在函数中创建的任何对象

你会注意到这些都不需要返回语句。你可能会认为构造函数是一个返回,而你正在做一些事情。实际上返回来自内存分配器。构造函数只是设置内存中的内容。只要新对象的封装没问题,这是可以的,因为当你制作它时,你可以完全控制它——没有其他人可以打破它。

访问其他对象的属性是正确的。Getter是错误的(但你知道它们已经很糟糕了,对吧?)。Setter是可以的,但最好使用构造函数。继承是糟糕的——当你从另一个类继承时,该类的任何更改都可能会让你崩溃。类型嗅探是糟糕的(是的——LoD意味着基于Java/C++样式的调度是不正确的——询问类型,甚至隐式地破坏封装。类型是对象的隐式属性。接口是正确的事情)。

那么为什么这一切都是一个问题呢?嗯,除非你的世界与我的世界非常不同,否则你会花费大量时间调试代码。你不是在编写你计划永远不会重用的代码。你的软件需求在变化,这导致了内部API/接口的变化。每次你使用返回语句时,你都引入了一个非常棘手的依赖关系——返回任何东西的方法都需要知道它们返回的东西将如何使用——这就是每种情况!一旦接口改变,不管是一端还是另一端,一切都可能中断,你将面临漫长而乏味的bug。

它们确实是您代码中的恶性癌症,因为一旦您开始使用它们,它们就会促进其他地方的进一步使用(这就是为什么您经常可以在对象系统中找到返回的方法链)。

那么替代方案是什么?

告诉不要问。

使用OOP-目标是告诉其他对象该做什么,并让他们照顾它。所以你必须忘记做事的过程方式。这真的很容易-只是永远不要写返回语句。有更好的方法来做同样的事情:

返回概念没有问题,但返回声明存在严重缺陷。

如果你真的需要一个答案——使用回调。传入一个要填写的数据结构,甚至。这样你就可以保持接口干净,对变化开放,你的整个系统不那么脆弱,适应性更强。它不会减慢你的系统速度,事实上它可以加快它,就像尾调用优化一样——除了在这种情况下,没有尾调用,所以你甚至不必浪费时间用返回值操作堆栈。

如果您遵循这些参数,您会发现确实需要从未返回语句。

如果你遵循这些做法,我保证很快你就会发现你花在寻找错误上的时间少了很多,更快地适应需求变化,并且在理解自己的代码方面遇到的问题也更少。

为了良好的标准行业最佳实践的利益,我们必须确定在所有函数中出现的返回语句的正确数量。显然,人们一致反对使用一个返回语句。所以我建议我们将其设置为两个。

如果每个人现在都能查看他们的代码,找到只有一个出口点的函数,然后添加另一个,我将不胜感激。

这种变化的结果无疑将是更少的错误,更大的易读性和难以想象的财富从天而降到我们的头上。

唯一重要的问题是“代码如何更简单、更易读、更容易理解?”如果它更简单,有多个返回,那么就使用它们。

我更喜欢单一的返回语句。一个尚未指出的原因是一些重构工具更适合单点退出,例如Eclipse JDT提取/内联方法。

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

嗯,也许我是这里少数几个年龄足够大的人之一,还记得为什么“只有一个返回语句”被如此努力地推送的主要原因之一。这是为了编译器可以发出更高效的代码。对于每个函数调用,编译器通常会推送堆栈上的一些寄存器以保留它们的值。这样,函数就可以使用这些寄存器进行临时存储。当函数返回时,那些保存的寄存器必须从堆栈中弹出并回到寄存器中。每个寄存器一个POP(或MOV-(SP), Rn)指令。如果你有一堆返回语句,那么要么每个语句都必须弹出所有寄存器(这会使编译后的代码变大),要么编译器必须跟踪哪些寄存器可能被修改,只弹出那些寄存器(减少代码大小,但增加编译时间)。

今天尝试坚持使用一个返回语句仍然有意义的一个原因是易于自动重构。如果您的IDE支持方法提取重构(选择一系列行并将它们转换为一个方法),如果您要提取的行中包含返回语句,则很难做到这一点,特别是如果您要返回一个值。

没有人提到或引用代码完成,所以我会这样做。

17.1回归

尽量减少每个例程中的返回次数。如果在底部阅读例程,您不知道它返回上面某个地方的可能性,则更难理解例程。

使用返回当它提高易读性时。在某些例程中,一旦你知道了答案,你就想立即将其返回到调用例程。如果例程的定义方式不需要任何清理,那么不立即返回意味着你必须编写更多代码。

有时出于性能原因,这是必要的(我不想获取与继续相同的不同缓存行;有时)。

如果您在不使用RAII的情况下分配资源(内存、文件描述符、锁等),那么多次返回可能容易出错,并且肯定是重复的,因为发布需要多次手动完成,您必须仔细跟踪。

在示例中:

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

我会把它写成:

function() {HRESULT error = OPERATION1FAILED;//assume failureif(SUCCEEDED(Operation1())) {
error = OPERATION2FAILED;//assume failureif(SUCCEEDED(Operation3())) {
error = OPERATION3FAILED;//assume failureif(SUCCEEDED(Operation3())) {
error = OPERATION4FAILED; //assume failureif(SUCCEEDED(Operation4())) {
error = S_OK;}}}}return error;}

这当然看起来更好。

这在手动资源发布的情况下尤其有用,因为在哪里以及哪些发布是必要的非常简单。如以下示例所示:

function() {HRESULT error = OPERATION1FAILED;//assume failureif(SUCCEEDED(Operation1())) {
//allocate resource for op2;char* const p2 = new char[1024];error = OPERATION2FAILED;//assume failureif(SUCCEEDED(Operation2(p2))) {
//allocate resource for op3;char* const p3 = new char[1024];error = OPERATION3FAILED;//assume failureif(SUCCEEDED(Operation3(p3))) {
error = OPERATION4FAILED; //assume failureif(SUCCEEDED(Operation4(p2,p3))) {
error = S_OK;}}//free resource for op3;delete [] p3;}//free resource for op2;delete [] p2;}return error;}

如果您在没有RAII(忘记异常问题!)的情况下编写此代码并使用多个出口,则必须多次写入删除。如果您使用}else{编写它,那么#36825;得有点难看

但是RAII使得多重退出资源问题没有实际意义。

作为指导方针,我投票支持最后的单一回报。这有助于公共代码清理处理…例如,查看以下代码…

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

是否有充分的理由说明在函数中只有一个返回语句是更好的做法?

,有:

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

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

更新:显然MISRA指南促进单一退出也是。

需要明确的是,我并不是说有多个返回值是错误的。但是,考虑到其他等价的解决方案,有很多很好的理由选择具有单个返回值的解决方案。

我强迫自己只使用一个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);}

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

  • 创建了许多可重用的功能,
  • 使函数更具可读性,并且
  • 函数的重点是为什么值是正确的。

拥有多个出口点与使用GOTO本质上是一样的。这是否是一件坏事取决于你对猛禽的感觉。

我认为在不同的情况下,不同的方法更好。例如,如果你应该在返回之前处理返回值,你应该有一个退出点。但在其他情况下,使用多个返回更舒服。

一个注意事项。如果您应该在几种情况下在返回之前处理返回值,但不是在所有情况下,定义像ProcessVal这样的方法并在返回之前调用它的最佳解决方案(IMHO):

var retVal = new RetVal();
if(!someCondition)return ProcessVal(retVal);
if(!anotherCondition)return retVal;

如果可以只写一个意见,那是我的:

我完全和绝对不同意“单一返回语句理论”,并发现它在代码易读性、逻辑和描述性方面大多是推测性的,甚至是破坏性的。

这种单返回的习惯对于纯过程编程来说甚至很差,更不用说更高级的抽象(函数式、组合式等)了。此外,我希望所有以这种风格编写的代码都经过一些特殊的重写解析器,使其具有多个返回语句!

一个函数(如果它真的是一个函数/查询,根据“查询-命令分离”注意-参见Eiffel编程语言。例如)必须定义与它拥有的控制流方案一样多的返回点。它更清晰,数学上更一致;这是编写功能(即查询)的方式

但我不会对你的代理确实收到的突变信息如此激进——程序调用。

我可能会为此感到讨厌,但理想情况下应该有返回语句,我认为,一个函数应该只返回它的最后一个表达式,并且在完全理想的情况下应该只包含一个。

所以不是

function name(arg) {if (arg.failure?)return;
//code for non failure}

而是

function name(arg) {if (arg.failure?)voidConstantelse {//code for non failure

}

对我来说,不是表达式和返回语句的if语句是一个非常可疑的做法。

我相信多次返回通常是好的(在我用C#编写的代码中)。单返回风格是C的延续。但你可能没有用C编码。

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

在C代码中,多个返回样式可能是一个坏习惯,资源必须显式地去分配,但是像Java,C#,Python或JavaScript这样的语言具有自动垃圾回收机制和try..finally块(以及C#中的using块)等结构,并且这个参数不适用-在这些语言中,需要集中手动资源释放是非常罕见的。

在某些情况下,单个返回更具可读性,在某些情况下不是。查看它是否减少了代码行数、使逻辑更清晰或减少了大括号、缩进或临时变量的数量。

因此,尽可能多地使用适合您的艺术敏感性的返回,因为这是一个布局和易读性问题,而不是技术问题。

我谈到了在我的博客上更详细

不,因为我们不再生活在70年代了。如果您的函数足够长,以至于多次返回是一个问题,那么它太长了。

(除了语言中任何有例外的多行函数无论如何都会有多个退出点这一事实之外。)

你知道格言-情人眼里出西施

有些人以NetBeans发誓,有些人以IntelliJ IDEA发誓,有些人以python发誓,有些人以php发誓。

在一些商店,如果你坚持这样做,你可能会失去工作:

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

问题在于可见性和可运维性。

我很喜欢用布尔代数来减少和简化逻辑以及状态机的使用。但是过去有些同事认为我这种“数学技巧”不合适,因为它不可见且不可维护,而且是一种不好的做法。抱歉,这些技巧对我来说是非常可见且可维护的——因为当我六个月后回到代码时,我会更清楚地理解代码,而不是看到一堆意大利面条。

嘿,伙计(就像以前的客户常说的)做你想做的,只要你知道如何修复它,当我需要你修复它。

记得20年前,我的一个同事因为采用了今天所谓的0战略而被解雇。他有一个细致入微的增量计划,但他的经理对他大喊大叫:“你不能增量地向用户发布功能!你必须坚持瀑布。”他对经理的回应是,增量开发将更精确地满足客户需求。他相信为客户需求开发,但经理相信按照“客户需求”编码。

我们经常因为破坏数据规范化、MVPMVC边界而感到内联。我们内联而不是构造函数。我们走捷径。

就个人而言,我认为PHP是一种糟糕的做法,但我知道什么。所有的理论争论归结为试图满足一组规则

质量=精度,可运维性盈利能力

所有其他规则都会消失在背景中。当然,这条规则永远不会消失:

懒惰是善的美德程序员。

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

我总是避免多个返回语句。即使在小函数中也是如此。小函数可以变得更大,跟踪多个返回路径使得(对我的小脑袋来说)更难跟踪正在发生的事情。单个返回也使调试更容易。我看到有人发帖说,多个返回语句的唯一替代方案是10层嵌套IF语句的凌乱箭头。虽然我肯定同意确实存在这样的编码,但这不是唯一的选择。我不会在多个返回语句和一窝IF之间做出选择,我会重构它,这样你就可以消除两者。这就是我的编码方式。以下代码消除了这两个问题,在我看来,很容易阅读:

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

我倾向于使用保护子句来提前返回,否则在方法结束时退出。单一进入和退出规则具有历史意义,在处理单个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语句,然后可以将其视为保护子句。同样,在一个做作的例子中,代码的意图并不明显,某位函数可能有一些其他副作用,或者结果可能会在//其余代码…中使用。

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

有人可能会争辩说……如果你有多个必须满足之前的条件来执行函数的任务,那么在满足这些条件之前不要调用函数:

而不是:

function doStuff(foo) {if (foo != null) return;}

function doStuff(foo) {if (foo !== null) {...}}

foo!=null之前不要调用doStuff

if(foo != null) doStuff(foo);

其中,要求每个调用站点确保援引的条件在调用前得到满足。如果有多个调用站点,这个逻辑最好放在一个单独的函数、待调用函数的方法(假设它们是一等公民)或代理中。

关于函数是否为数学上可以证明的话题,请考虑语法上的逻辑。如果一个函数有多个返回点,这并不意味着(默认情况下)它不能用数学证明。

这主要是Fortran的遗留问题,它可以将多个语句标签传递给一个函数,以便它可以返回到其中的任何一个。

所以这种代码是完全有效的

       CALL SOMESUB(ARG1, 101, 102, 103)C Some code101   CONTINUEC Some more code102   CONTINUEC Yet more code103   CONTINUEC You get the general idea

但是被调用的函数决定了你的代码路径去了哪里。有效?可能。可维护?不。

这就是规则的由来(顺便说一句,函数没有多个入口点,这在fortran和汇编器中是可能的,但在C中不是)。

然而,它的措辞看起来可以应用于其他语言(关于多个入口点的规则不能应用于其他语言,所以它不是真正的程序)。所以这条规则被保留了下来,即使它指的是一个完全不同的问题,并且不适用。

对于结构化程度更高的语言来说,这条规则需要丢弃,或者至少需要深思熟虑。当然,一个带有返回值的函数很难理解,但从一开始就返回不是问题。在某些C++编译器中,如果你只从一个地方返回一个值,一个返回点可能会生成更好的代码。

但是最初的规则被误解了,被误用了。不再相关。

您可以这样做以仅实现一个返回语句-在开始时声明它并在结束时输出它-问题解决了:

$content = "";$return = false;
if($content != ""){$return = true;}else{$return = false;}
return $return;