是否有充分的理由说明在函数中只有一个 return 语句是一种更好的做法?
或者只要在逻辑上正确就可以从函数返回,这意味着函数中可能有许多返回语句?
是否有充分的理由说明在函数中只有一个 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 条件和大括号正确匹配。
这种方法使代码维护极其困难并且容易出错。
正如 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 或其他自动内存管理,那么你必须为每次返回进行清理,这要么意味着剪切和粘贴清理或 goto (逻辑上与托管语言中的“最终”相同),这两者都被认为是错误的形式。如果您的实践是在 C++ 或其他自动内存系统中使用智能指针和集合,那么没有充分的理由这样做,而这一切都与可读性有关,更多的是一种判断调用。
我倾向于认为函数中间的 return 语句是不好的。您可以使用返回在函数顶部构建一些保护子句,当然可以毫无问题地告诉编译器在函数末尾返回什么,但函数中间的返回很容易错过并且可以使函数更难解释。
是否有充分的理由说明在函数中只有一个 return 语句是一种更好的做法?
是的,有:
这个问题通常被认为是多重返回或深度嵌套的 if 语句之间的错误二分法。几乎总是有第三种解决方案,它是非常线性的(没有深度嵌套),只有一个出口点。
更新:显然MISRA 指南也提倡单一出口。
需要明确的是,我并不是说多次退货总是错误的。但是考虑到其他等效的解决方案,有很多充分的理由更喜欢单一回报的解决方案。
拥有单个退出点确实在调试中提供了优势,因为它允许您在函数末尾设置单个断点以查看实际将返回的值。
一般来说,我试图从一个函数中只有一个退出点。然而,有时这样做实际上最终会创建一个比必要的更复杂的函数体,在这种情况下,最好有多个退出点。它确实必须是基于产生的复杂性的“判断电话”,但目标应该是在不牺牲复杂性和可理解性的情况下尽可能少的退出点。
我更喜欢单次退出,除非它真的使事情复杂化。我发现在某些情况下,多个存在点可以掩盖其他更重要的设计问题:
public void DoStuff(Foo foo)
{
if (foo == null) return;
}
看到这段代码,我会立即问:
根据这些问题的答案,可能是
在上述两种情况下,代码可能都可以通过断言来重新设计,以确保 'foo' 永远不会为 null 并且相关的调用者已更改。
还有另外两个原因(我认为具体到 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'
}
如果代码大小是一个问题 - 那么这可能是值得避免的事情。
另一个问题与“命名返回值优化”(又名复制省略,ISO C++ '03 12.8/15)有关。如果可以,C++ 允许实现跳过调用复制构造函数:
A foo () {
A a1;
// do something
return a1;
}
void bar () {
A a2 ( foo() );
}
照原样看代码,对象“a1”在“foo”中构造,然后将调用其复制构造来构造“a2”。但是,复制省略允许编译器在堆栈上与“a2”相同的位置构造“a1”。因此,当函数返回时,无需“复制”对象。
多个退出点使编译器在尝试检测这一点时的工作变得复杂,并且至少对于相对较新的 VC++ 版本,在函数体有多个返回的情况下没有进行优化。有关详细信息,请参阅Visual C++ 2005 中的命名返回值优化。
不,因为我们不再生活在 1970 年代。如果你的函数足够长以至于多次返回是个问题,那就太长了。
(除此之外,语言中的任何多行函数都存在异常,但无论如何都会有多个退出点。)
拥有一个退出点可以降低循环复杂度,因此理论上可以降低在更改代码时将错误引入代码的可能性。然而,实践往往表明需要一种更务实的方法。因此,我的目标是拥有一个退出点,但如果这样更具可读性,则允许我的代码有多个退出点。
我强迫自己只使用一个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);
}
诚然,它更长而且有点混乱,但是在以这种方式重构函数的过程中,我们已经
我会说你应该有尽可能多的要求,或者任何使代码更清晰的东西(比如保护子句)。
我个人从未听过/见过任何“最佳实践”说您应该只有一份退货声明。
在大多数情况下,我倾向于基于逻辑路径尽快退出函数(保护子句就是一个很好的例子)。
有一个单一的出口点有好处,正如不可避免的“箭头”编程产生的坏话一样。
如果在输入验证或资源分配期间使用多个退出点,我会尝试将所有“错误退出”非常明显地放在函数的顶部。
“SSDSLPedia”的Spartan Programming文章和“Portland Pattern Repository's 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 语句(或者根本不会使用它,如果我可以毫不费力地摆脱它),并且代码会更加简单。并且非箭头。大多数非最终回报可能是例外。
简而言之;
我相信多次返回通常是好的(在我用 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 示例中,问题是//Rest of code...中发生了什么?:
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()可能有其他一些副作用,或者结果可能会在// Rest of code... 中使用。
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”好不了多少。它有单个 return 语句,但函数实际结束的点有多个。
如果您有多个返回(或“res =”)的函数,则将其分解为具有单个退出点的几个较小的函数通常是个好主意。
我通常的策略是在函数末尾只有一个 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]
}
请注意,这只是一个简单的示例
作为指导,我最后投票支持单次返回。这有助于常见的代码清理处理......例如,看看下面的代码......
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 语句越多,该方法的复杂性就越高。如果您发现自己想知道是否有太多的 return 语句,您可能想问自己该函数中是否有太多的代码行。
但是,不是,一个/多个返回语句没有错。在某些语言中,它是一种更好的实践 (C++),而不是其他语言 (C)。
这可能是一个不寻常的观点,但我认为任何认为应该支持多个返回语句的人都不必在仅支持 4 个硬件断点的微处理器上使用调试器。;-)
虽然“箭头代码”的问题是完全正确的,但在使用多个 return 语句时似乎会消失的一个问题是在您使用调试器的情况下。您没有方便的包罗万象的位置来放置断点以保证您将看到出口并因此看到返回条件。
如果可以只写一个意见,那就是我的:
我完全和绝对不同意“单一返回语句理论”,并发现它在代码可读性、逻辑和描述性方面大多是推测性的,甚至是破坏性的。
单次返回的习惯对于纯过程编程来说甚至是糟糕的,更不用说更高级的抽象(函数式、组合式等)了。此外,我希望所有以这种风格编写的代码都经过一些特殊的重写解析器,使其具有多个返回语句!
一个函数(如果它真的是一个根据“查询命令分离”注释的函数/查询——例如,参见 Eiffel 编程语言)必须定义与其所具有的控制流场景一样多的返回点。它更加清晰且在数学上更加一致;它是编写函数的方式(即查询)
但我不会对你的代理收到的突变信息如此激进——程序调用。
总是需要一个返回类型是没有意义的。我认为这更像是一个标志,可能需要简化一些事情。有时有必要有多个返回,但通常你可以通过至少尝试有一个退出点来让事情变得更简单。
唯一重要的问题是“代码如何更简单、更易读、更容易理解?” 如果多次返回更简单,请使用它们。
拥有多个退出点与使用GOTO
. 这是否是一件坏事取决于你对猛禽的看法。
您已经隐式地有多个由错误处理引起的隐式返回语句,因此请处理它。
但是,与编程一样,有支持和反对多重返回实践的示例。如果它使代码更清晰,请以一种或另一种方式进行。使用许多控制结构会有所帮助(例如case语句)。
好吧,也许我是这里为数不多的人之一,足以记住“只有一份回报声明”被如此努力推动的重要原因之一。这样编译器就可以发出更高效的代码。对于每个函数调用,编译器通常会将一些寄存器压入堆栈以保留它们的值。这样,该函数可以使用这些寄存器进行临时存储。当函数返回时,那些保存的寄存器必须从堆栈中弹出并返回到寄存器中。这是每个寄存器一个 POP(或 MOV -(SP),Rn)指令。如果您有一堆返回语句,那么每个都必须弹出所有寄存器(这会使编译的代码更大),或者编译器必须跟踪哪些寄存器可能已被修改并且只弹出那些(减少代码大小,但增加了编译时间)。
今天尝试坚持使用一个 return 语句仍然有意义的一个原因是易于自动重构。如果您的 IDE 支持方法提取重构(选择一系列行并将它们转换为方法),那么如果您要提取的行中包含 return 语句,则很难做到这一点,特别是如果您要返回一个值.
我使用多个退出点来使错误情况+处理+返回值尽可能接近。
因此,必须测试必须为真的条件 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 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”参数中的值。
为了良好的标准和行业最佳实践,我们必须建立正确数量的返回语句以出现在所有函数中。显然,人们一致反对有一份退货声明。所以我建议我们将它设置为两个。
如果每个人现在都可以查看他们的代码,找到只有一个退出点的任何函数,然后添加另一个退出点,我将不胜感激。在哪里都无所谓。
这种变化的结果无疑将是更少的错误、更高的可读性和难以想象的财富从天上掉到我们的头上。
我更喜欢单一的 return 语句。尚未指出的一个原因是某些重构工具更适合单点退出,例如 Eclipse JDT 提取/内联方法。
有时出于性能原因需要这样做(我不想获取与继续需要相同的不同缓存行类型;有时)。
如果您在不使用 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 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;
}
这当然看起来更好。
这在手动资源释放的情况下特别有用,因为在哪里以及哪些版本是必要的非常简单。如下例所示:
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 的情况下编写此代码(忘记了异常问题!)有多个退出,则必须多次编写删除。如果你用它来写它,}else{
它会变得有点难看。
但 RAII 使多出口资源问题变得毫无意义。
我总是避免使用多个 return 语句。即使在小型功能中。小函数可以变得更大,并且跟踪多个返回路径使(在我看来)更难跟踪正在发生的事情。单次返回也使调试更容易。我见过有人发帖说,多个 return 语句的唯一替代方法是嵌套 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;
}
作为嵌套 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;
}
这给你一个出口点,让你有其他的操作嵌套,但仍然不是一个真正的深层结构。如果你不喜欢 !SUCCEEDED 你总是可以做 FAILED 任何事情。这种事情还允许您在任何两个其他检查之间添加其他代码,而无需重新缩进任何内容。
如果你真的疯了,整个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);
我认为在不同的情况下不同的方法更好。例如,如果你应该在返回之前处理返回值,你应该有一个退出点。但在其他情况下,使用多个回报会更舒服。
一注。如果您应该在几种情况下(但不是全部)在返回之前处理返回值,最好的解决方案(恕我直言)来定义像 ProcessVal 这样的方法并在返回之前调用它:
var retVal = new RetVal();
if(!someCondition)
return ProcessVal(retVal);
if(!anotherCondition)
return retVal;
我可能会因此而被讨厌,但理想情况下,我认为根本不应该有return语句,一个函数应该只返回它的最后一个表达式,并且在完全理想的情况下应该只包含一个。
所以不是
function name(arg) {
if (arg.failure?)
return;
//code for non failure
}
反而
function name(arg) {
if (arg.failure?)
voidConstant
else {
//code for non failure
}
不是表达式和 return 语句的 if 语句对我来说是一种非常可疑的做法。
有人可能会争辩说……如果在执行函数的任务之前必须满足多个条件,那么在满足这些条件之前不要调用函数:
代替:
function doStuff(foo) {
if (foo != null) return;
}
或者
function doStuff(foo) {
if (foo !== null) {
...
}
}
在foo != nulldoStuff
之前不要调用
if(foo != null) doStuff(foo);
这要求每个调用站点确保在调用之前满足调用条件。如果有多个调用站点,这个逻辑可能最好放在一个单独的函数中,在要调用的函数的方法中(假设它们是一等公民),或者在代理中。
关于函数在数学上是否可证明的话题,请考虑语法之上的逻辑。如果一个函数有多个返回点,这并不意味着(默认情况下)它在数学上是不可证明的。
这主要是 Fortran 的遗留问题,可以将多个语句标签传递给一个函数,以便它可以返回到其中的任何一个。
所以这种代码是完全有效的
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++ 编译器中,如果您只从一个位置返回值,则单个返回点可能会生成更好的代码。
但是原来的规则被误解了,被误用了。并且不再相关。
您可以这样做以仅实现一个返回语句 - 在开始时声明并在结束时输出 - 问题已解决:
$content = "";
$return = false;
if($content != "")
{
$return = true;
}
else
{
$return = false;
}
return $return;
我通常赞成多个返回语句。它们最容易阅读。
也有不好的情况。有时从函数返回可能非常复杂。我记得一种情况,所有函数都必须链接到多个不同的库中。一个库期望返回值是错误/状态代码,而其他库则不是。有一个单一的 return 语句可以节省时间。
我很惊讶没有人提到 goto。Goto 并不是每个人都希望你相信的编程的祸根。如果您必须在每个函数中只有一个 return,请将其放在末尾并根据需要使用 goto 跳转到该 return 语句。绝对避免既丑陋又运行缓慢的标志和箭头编程。
如果管理得当,多次退出很好
第一步是明确退出的原因。我的一般是这样的:
1.不需要执行函数
2.发现错误
3.提前完成
4.正常完成
我想你可以将“1.不需要执行函数”分组为“3.提前完成” (如果你愿意的话,一个非常早的完成)。
第二步,让函数外的世界知道退出的原因。伪代码如下所示:
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
显然,如果将上图中的一组工作移到一个单独的函数中是有益的,那么您应该这样做。
如果您愿意,您可以更具体地说明退出状态,例如,使用几个错误代码和提前完成代码来确定退出的原因(甚至位置)。
即使您将此功能强制为只有一个退出的功能,我认为您仍然需要指定退出状态。调用者需要知道是否可以使用输出,这有助于维护。
我知道我会为此而兴奋,但我是认真的。
返回语句基本上是程序编程时代的遗留物。它们是 goto 的一种形式,以及 break、continue、if、switch/case、while、for、yield 和其他一些语句以及大多数现代编程语言中的等价物。
返回语句有效地“转到”调用函数的点,在该范围内分配一个变量。
Return 语句是我所说的“方便的噩梦”。他们似乎可以快速完成工作,但会导致大量的维护问题。
这是面向对象编程中最重要和最基本的概念。这是 OOP 存在的理由。
每当您从方法返回任何内容时,您基本上都是从对象中“泄漏”状态信息。不管你的状态是否改变,也不管这个信息是否来自其他对象——它对调用者没有任何影响。这样做是允许对象的行为在对象之外——破坏封装。它允许调用者以导致脆弱设计的方式开始操作对象。
我建议任何开发人员在 c2.com 或 Wikipedia 上阅读得墨忒耳定律(LoD)。LoD 是一种设计理念,已在字面意义上具有真正的“关键任务”软件约束的地方使用,例如 JPL。它已被证明可以减少代码中的错误数量并提高灵活性。
有一个很好的类比是基于遛狗。当你遛狗时,你并没有抓住它的腿并移动它们让狗走路。你命令狗走路,它会照顾自己的腿。这个类比中的 return 语句相当于狗让你抓住它的腿。
您会注意到这些都不需要 return 声明。你可能认为构造函数是一个返回,而你正在做某事。实际上返回来自内存分配器。构造函数只是设置内存中的内容。只要该新对象的封装是可以的,就可以了,因为当您创建它时,您可以完全控制它 - 没有其他人可以破坏它。
访问其他对象的属性是正确的。吸气剂已经出局了(但你知道它们已经很糟糕了,对吧?)。Setter 可以,但最好使用构造函数。继承是不好的——当你从另一个类继承时,该类中的任何更改都可能而且可能会破坏你。类型嗅探很糟糕(是的 - LoD 意味着基于 Java/C++ 样式类型的调度是不正确的 - 询问类型,即使是隐式的,也会破坏封装。类型是对象的隐式属性。接口是正确的东西)。
那么为什么这一切都是个问题呢?好吧,除非你的世界与我的完全不同,否则你会花费大量时间调试代码。您不是在编写您计划永远不会重用的代码。您的软件需求正在发生变化,这会导致内部 API/接口发生变化。每次使用 return 语句时,都会引入一个非常棘手的依赖项——返回任何内容的方法都需要知道它们返回的内容将如何使用——这是每一种情况!一旦界面发生变化,一方面,一切都可能中断,您将面临漫长而乏味的错误搜索。
它们在你的代码中确实是一种毒瘤,因为一旦你开始使用它们,它们就会促进在其他地方的进一步使用(这就是为什么你经常可以在对象系统中找到返回的方法链的原因)。
那么替代方案是什么?
使用 OOP - 目标是告诉其他对象该做什么,并让他们处理它。所以你必须忘记做事的程序方式。这真的很容易——只是永远不要写返回语句。有更好的方法来做同样的事情:
如果您真的需要回复 - 使用回电。传入一个要填写的数据结构,偶。这样一来,您就可以保持界面清洁和开放以适应变化,并且您的整个系统不那么脆弱并且更具适应性。它不会减慢您的系统速度,实际上它可以加快系统速度,就像尾调用优化一样 - 除了在这种情况下,没有尾调用,因此您甚至不必浪费时间来操作堆栈返回值。
如果你遵循这些论点,你会发现真的不需要return 语句。
如果你遵循这些实践,我保证很快你就会发现你花费更少的时间来寻找错误,更快地适应需求变化,并且在理解你自己的代码时遇到更少的问题。