在void方法中使用return语句是一种不好的编程习惯吗?

106

假设有以下代码:

void DoThis()
{
    if (!isValid) return;

    DoThat();
}

void DoThat() {
    Console.WriteLine("DoThat()");
}

在void方法中使用return是否可行?这样做是否会对性能产生影响?还是最好像这样编写代码:

void DoThis()
{
    if (isValid)
    {
        DoThat();
    }
}

1
怎么样:void DoThis() {if (isValid) DoThat();} - Dscoduc
34
想象一下代码?为什么?它就在那里!:-D - STW
这是一个好问题,我一直在思考使用return;退出方法或函数是否是一个好的实践。特别是在一个具有多个IQueryable<T>结果且它们彼此依赖的LINQ数据挖掘方法中。如果其中一个没有结果,则警告并退出。 - Cheung
11个回答

191
在一个空方法中使用return语句并不是不好的做法,将if语句反转以减少嵌套是一种常见做法。同时,减少方法嵌套可以提高代码可读性和可维护性。
实际上,如果你有一个没有任何返回语句的空方法,编译器会在其末尾始终生成ret指令

37

使用守卫(而非嵌套代码)的另一个极好的原因在于:如果另一个程序员向您的函数添加代码,则他们将在更安全的环境中工作。

考虑以下情况:

void MyFunc(object obj)
{
    if (obj != null)
    {
        obj.DoSomething();
    }
}

对比:

void MyFunc(object obj)
{
    if (obj == null)
        return;

    obj.DoSomething();
}

现在,想象另一个程序员添加了这行代码:obj.DoSomethingElse();

void MyFunc(object obj)
{
    if (obj != null)
    {
        obj.DoSomething();
    }

    obj.DoSomethingElse();
}

void MyFunc(object obj)
{
    if (obj == null)
        return;

    obj.DoSomething();
    obj.DoSomethingElse();
}

显然这只是一个简单的例子,但在第一个(嵌套代码)示例中,程序员已经在程序中添加了一次崩溃。在第二个示例(使用守卫早期退出),一旦通过了守卫,您的代码就可以安全地避免意外使用空引用。

当然,优秀的程序员不会经常犯这样的错误。但是预防胜于治疗——我们可以通过编写消除此潜在错误源的代码来消除这个问题。嵌套会增加复杂度,因此最佳实践建议重构代码以减少嵌套。


是的,但另一方面,多层嵌套以及它们的条件使代码更容易出现错误,逻辑更难追踪,而且更重要的是,很难调试。在我看来,扁平函数是较小的邪恶。 - Skrim
20
我支持减少嵌套! :-) - Jason Williams
我同意这个观点。此外,从重构的角度来看,如果obj变成一个结构体或者你可以保证不会为空的东西,那么重构方法会更容易和更安全。 - Phil Cooper

20

不好的实践?绝对不是这样。事实上,如果验证失败,则尽早从方法中返回来处理验证总是更好的做法。 否则,将导致大量嵌套的if和else语句。提前终止有助于提高代码可读性。

还要查看类似问题的答案:我应该使用return/continue语句还是if-else语句?


9

这并不是一个坏的做法(因为之前已经提到了所有原因)。然而,如果一个方法中有很多返回值,那么它应该被拆分成更小的逻辑方法。


有时候返回值是基于其他较小的逻辑方法的结果。例如,isSomeCondition = IsSomeCondition(someCriteria) 如果(!isSomeCondition) 返回; isSomeOtherCondition = IsSomeOtherCondition(someOtherCriteria) 如果(!isSomeOtherCondition) 返回; - Louise Eggleton

8
第一个例子是使用守卫语句。来自维基百科的说明如下:
“在计算机编程中,守卫是一个布尔表达式,如果程序执行要继续在所讨论的分支中,则必须评估为true。”
我认为在方法顶部有一堆守卫是一种完全可以理解的编程方式。它基本上是在说“如果这些条件中有任何一个为true,则不要执行此方法”。
因此,通常会像这样:
void DoThis()
{
  if (guard1) return;
  if (guard2) return;
  ...
  if (guardN) return;

  DoThat();
}

我认为这比下面的更易读:

void DoThis()
{
  if (guard1 && guard2 && guard3)
  {
    DoThat();
  }
}

3

没有性能惩罚,但第二段代码更易读,因此更容易维护。


1
Russell,我不同意你的观点,但你不应该因此被踩。+1来平衡一下。顺便说一句,我认为在单行上进行布尔测试和返回,然后再空一行是清楚地表明正在发生什么的明显指示。例如,Rodrigo的第一个例子。 - Paul Sasik
1
我不同意这个观点。增加嵌套并不能提高可读性。第一段代码使用了“守卫”语句,这是一个完全可以理解的模式。 - cdmckay
1
我也不同意。现在,提前退出函数的守卫子句通常被认为是有助于读者理解实现的好方法。 - Pete Hodgson

2
在这种情况下,你的第二个例子是更好的代码,但这与从void函数返回没有任何关系,只是因为第二个代码更加直接。但是从void函数返回完全没有问题。

1

这是完全可以的,没有“性能惩罚”,但永远不要写一个没有括号的“if”语句。

始终如此。

if( foo ){
    return;
}

这样代码更易读,你也不会错误地假设某些代码部分在该语句内。


2
可读性是主观的。在我看来,任何不必要的代码添加都会降低其可读性...(我需要阅读更多内容,然后想知道它为什么存在,浪费时间确保我没有错过什么)...但这只是我的主观看法。 - Charles Bretana
10
始终包含花括号的更好原因不是为了可读性,而是为了安全。如果没有花括号,有人以后修复需要将其他语句作为if的一部分添加的错误,没有足够仔细地注意并添加它们,这种错误很容易发生。通过始终包含花括号,可以消除这种风险。 - Scott Dorman
2
Silky,请在你的 { 前按下回车键。这将使你的 {} 在同一列中对齐,大大提高了可读性(更容易找到相应的开/闭括号)。 - Imagist
1
@Imagist 我会将这个留给个人喜好;而且它是按照我自己的喜欢完成的 :) - Noon Silk
1
如果每个右括号都与在同一缩进级别上放置的左括号匹配,那么视觉上区分哪些 if 语句需要右括号将变得容易,因此将 if 语句控制单个语句将是安全的。将左括号推回到 if 所在行会为每个多语句 if 节省一行垂直空间,但这将需要使用一个否则不必要的右括号行。 - supercat
显示剩余2条评论

0

我要在这个问题上不同意你们所有的年轻人。

在方法中使用return,无论是void还是其他类型,都是非常糟糕的做法,这是由已故的Edsger W. Dijkstra在近40年前清晰地阐述的原因,从著名的“GOTO语句有害”开始,一直到Dahl、Dijkstra和Hoare的“结构化编程”。

基本规则是每个控制结构和每个模块都应该有一个入口和一个出口。在模块中显式使用return会打破这个规则,并且使得程序状态更难以理解,进而使得判断程序是否正确变得更加困难(这比“是否能正常工作”更强的属性)。

“GOTO语句有害”和“结构化编程”开启了20世纪70年代的“结构化编程”革命。这两篇文章是我们今天拥有if-then-else、while-do和其他显式控制结构的原因,也是为什么高级语言中的GOTO语句在濒危物种名单上的原因。(我的个人观点是它们需要被列入灭绝物种名单。)

值得注意的是,消息流调制器是第一款军用软件,它在第一次尝试通过验收测试时没有出现任何偏差、豁免或“但是”的措辞,而且它是用一种甚至没有GOTO语句的语言编写的。
另外值得一提的是,尼古拉斯·维尔特在Oberon-07中改变了RETURN语句的语义,使其成为一个类型化过程(即函数)声明的尾随部分,而不是函数体中的可执行语句。他对这个变化的解释是,他之所以这样做,正是因为先前的形式违反了结构化编程的单一退出原则。

2
@John:我们刚刚克服了Dykstra关于多重返回的禁令,就在我们克服Pascal的时候(至少我们大部分人是这样)。 - John Saunders
1
守卫语句和goto之间存在很大的区别。goto的问题在于它们可以跳转到任何地方,因此可能非常令人困惑和难以理解。守卫语句恰恰相反-它们为方法提供了一个带门的入口,在此之后,您知道您正在一个“安全”的环境中工作,减少了您在编写其余代码时需要考虑的事情数量(例如,“我知道这个指针永远不会为空,所以我不需要在整个代码中处理该情况”)。 - Jason Williams
@John:我现在对于多个返回值的问题比以前结构化编程(面向对象之前)时少得多。在良好的代码中,我不需要过多地推理方法做了什么或没有做什么 - 我假设它完成了它命名的任务,或者它会抛出异常,有时返回“false”。 - John Saunders
在过去的近十年中,人们进行单元测试的次数越来越多。除了更多的开发人员遵循“SOLID”面向对象编程原则之外,其中第一个原则是“单一职责”,单元测试也在某种程度上迫使方法变得非常非常小。在这些情况下,“早期返回”几乎是不可能的,但在合理的情况下,它们不会对代码可读性造成任何可记录的负面影响。这些方法平均只有8行代码!重点是,如果你的代码中似乎存在问题,那么你的代码可能需要重构。 - Suamere
@Suamere:重新阅读后,我发现有必要加上Dijkstra的一句话:“程序测试可以用来显示错误的存在,但永远不能用来显示它们的不存在!”[EWD 249,“结构化编程笔记”,1970年]。50年前的话。 - John R. Strohm
显示剩余5条评论

0

在使用守卫时,请确保遵循某些准则,以免混淆读者。

  • 函数只做一件事情
  • 守卫仅作为函数中的第一个逻辑引入
  • 未嵌套部分包含函数的核心意图

示例

// guards point you to the core intent
void Remove(RayCastResult rayHit){

  if(rayHit== RayCastResult.Empty)
    return
    ;
  rayHit.Collider.Parent.Remove();
}

// no guards needed: function split into multiple cases
int WonOrLostMoney(int flaw)=>
  flaw==0 ? 100 :
  flaw<10 ? 30 :
  flaw<20 ? 0 :
  -20
;

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接