反转“ if”语句以减少嵌套


272

例如,当我在代码上运行ReSharper时:

    if (some condition)
    {
        Some code...            
    }

ReSharper给了我以上警告(反转“ if”语句以减少嵌套),并建议进行以下更正:

   if (!some condition) return;
   Some code...

我想了解为什么会更好。我一直以为在方法中间使用“返回”是有问题的,有点像“转到”。


1
我相信异常检查并在开始时返回就可以了,但是我会更改条件,以便您直接检查异常,而不是不进行检查(即是否返回(某些条件))。
bruceatk

36
不,它不会提高性能。
塞斯·卡内基

3
如果我的方法传递的是错误的数据,我很想抛出ArgumentException。
asawyer 2011年

1
@asawyer是的,这里有一个关于功能过于宽泛的无聊的讨论,而不是使用断言失败。编写Solid Code令我大开眼界。在这种情况下,这将类似于ASSERT( exampleParam > 0 )
格雷格·亨德肖特

4
断言用于内部状态,而不是参数。您首先要验证参数,断言内部状态正确,然后执行操作。在发布版本中,您可以省去断言,也可以将它们映射到组件关闭。
西蒙·里希特

Answers:


296

方法中间的返回不一定不好。如果它使代码的意图更加清晰,最好立即返回。例如:

double getPayAmount() {
    double result;
    if (_isDead) result = deadAmount();
    else {
        if (_isSeparated) result = separatedAmount();
        else {
            if (_isRetired) result = retiredAmount();
            else result = normalPayAmount();
        };
    }
     return result;
};

在这种情况下,如果_isDead为true,我们可以立即退出该方法。最好这样构造它:

double getPayAmount() {
    if (_isDead)      return deadAmount();
    if (_isSeparated) return separatedAmount();
    if (_isRetired)   return retiredAmount();

    return normalPayAmount();
};   

我已经从重构目录中选择了此代码。这种特定的重构称为:用Guard子句替换嵌套条件。


13
这是一个非常好的例子!重构后的代码更像是一个case语句。
阿瑟赛德

16
可能只是一个口味问题:我建议将第二和第三“ if”更改为“ else if”,以进一步提高可读性。如果忽略了“ return”语句,则仍然很清楚,只有在前一个失败的情况下才检查以下情况,即检查的顺序很重要。
foraidt

2
在一个示例中,这个简单的id同意第二种方法更好,但这仅是因为它是如此明显。
安德鲁·布洛克(

现在,我们如何处理那段漂亮的代码jop编写的内容,并防止Visual Studio将其分解并将收益分成几行?它以这种方式重新格式化代码确实让我感到不快。使此代码可读性很差。
马克T

@Mark T Visual Studio中有一些设置可以防止其破坏代码。
亚伦·史密斯,

333

这不仅美观,而且还降低了方法内部的最大嵌套级别。通常认为这是一个加号,因为它使方法更易于理解(实际上,许多 静态 分析 工具都将其作为代码质量的指标之一进行了度量)。

另一方面,这也使您的方法具有多个退出点,另一组人认为这是不行的。

就个人而言,我同意ReSharper和第一小组的意见(在一种有例外的语言中,我发现讨论“多个出口点”很愚蠢;几乎所有东西都可以抛出,因此所有方法中都有许多潜在的出口点)。

关于性能:两种版本在每种语言中应该等效(如果不是IL级别的,那么一定要在抖动通过代码之后)。从理论上讲,这取决于编译器,但是实际上,当今任何广泛使用的编译器都能够处理比这更高级的代码优化案例。


41
单出口?谁需要它?
sq33G 2011年

3
@ sq33G:关于SESE(当然还有答案)的问题太棒了。感谢您的链接!
乔恩

我一直在回答,有人倡导单一出口点,但我从未见过有人真正提倡这样做,尤其是在C#之类的语言中。
Thomas Bonini 2011年

1
@AndreasBonini:缺席证明不是缺席证明。:-)
乔恩(Jon)

是的,当然,我只是感到奇怪,每个人都觉得有必要说一些人喜欢另一种方法,如果这些人自己不需要说这句话=)
Thomas Bonini 2011年

102

这有点宗教性,但是我同意ReSharper的观点,即您应该减少嵌套。我认为这要比从一个函数中拥有多个返回路径的缺点更大。

减少嵌套的主要原因是提高代码的可读性和可维护性。请记住,将来许多其他开发人员将需要阅读您的代码,并且缩进较少的代码通常更容易阅读。

前提条件是一个很好的例子,可以在函数开始时尽早返回。为什么前提条件检查会影响其余功能的可读性?

至于从方法中多次返回的负面影响-调试器现在功能非常强大,而且很容易找出确切的位置和何时返回特定函数。

一个函数中有多个返回值不会影响维护程序员的工作。

代码可读性差。


2
函数中的多次返回确实会影响维护程序员。调试函数时,在寻找恶意返回值时,维护人员必须在所有可以返回的位置上放置断点。
EvilTeach

10
我会在开括号处设置一个断点。如果逐步执行该功能,不仅可以看到按顺序执行的检查,而且可以很清楚地知道在该运行中该功能落在了哪个检查上。
John Dunagan

3
在这里与John保持一致...我只是单步执行整个功能就不会看到问题。
钉子

5
@Nailer很晚了,我特别同意,因为如果该函数太大而无法执行,则无论如何应将其分成多个函数!
Aidiakapi 2012年

1
也同意约翰。也许这是一个老派的想法,将断点放在底部,但是如果您想知道函数将返回什么,那么您将逐步遍历该函数以查看它为何返回所返回的内容。如果您只想查看返回的内容,则将其放在最后一个括号中。
user441521

70

正如其他人提到的那样,不应该影响性能,但是还有其他考虑因素。除了那些有效的顾虑,在某些情况下,这也可以使您轻松自如。假设您正在处理一个double

public void myfunction(double exampleParam){
    if(exampleParam > 0){
        //Body will *not* be executed if Double.IsNan(exampleParam)
    }
}

将其与看似等效的反转相反:

public void myfunction(double exampleParam){
    if(exampleParam <= 0)
        return;
    //Body *will* be executed if Double.IsNan(exampleParam)
}

因此,在某些情况下,看起来正确的a if可能并非正确。


4
还应该注意,重新共享工具快速修复程序将exampleParam> 0转换为exampleParam <0 而不是 exampleParam <= 0,这使我陷入困境。
昵刻

1
这就是为什么检查应该提前并退还给开发人员的原因,因为开发人员认为nan应该导致纾困。
user441521

51

只在函数末尾返回的想法是从语言支持异常的日子开始的。它使程序能够依赖于将清除代码放在方法的末尾,然后确保将其调用,并且其他程序员不会在方法中隐藏导致清除代码被跳过的返回值。跳过的清除代码可能会导致内存或资源泄漏。

但是,在支持异常的语言中,它不提供此类保证。在支持异常的语言中,任何语句或表达式的执行都会导致控制流,该控制流导致方法结束。这意味着必须使用finally或关键字进行清理。

无论如何,我是说我认为很多人都引用了“仅在方法末尾返回”的准则,却不理解为什么这样做是一件好事,而减少嵌套以提高可读性可能是一个更好的目标。


6
您刚刚弄清楚了为什么异常是UglyAndEvil [tm] ...; ;-)异常是花哨的,昂贵的伪装中的恶魔。
EricSchaefer

16
@Eric您一定遇到了非常糟糕的代码。当错误使用它们时,这很明显,并且它们通常允许您编写更高质量的代码。
罗伯特·保尔森

这就是我真正想要的答案!这里有很多不错的答案,但是其中大多数只是针对示例,而不是针对此建议产生的实际原因。
古斯塔沃·莫里

这是最好的答案,因为它解释了为什么整个争论首先出现。
Buttle Butkus 2013年

If you've got deep nesting, maybe your function is trying to do too many things.=>这不是您以前的短语的正确结果。因为就在您说可以将代码C的行为A重构为代码D的行为A之前。代码D是更干净的,允许的,但是“太多的事情”是指行为,它没有改变。因此,您对这个结论没有任何意义。
v.oddou 2014年

30

我想补充一下,那些倒置的if的名字是-Guard Clause。我会尽可能使用它。

我讨厌在开头有两个屏幕的代码,而没有其他地方阅读代码。只要反转,然后返回即可。这样,没有人会浪费时间滚动。

http://c2.com/cgi/wiki?GuardClause


2
究竟。它更多是一种重构。正如您提到的,它有助于更​​轻松地阅读代码。
rpattabi 2011年

对于保护条款而言,“返回”是不够的且可怕的。我会做:if(ex <= 0)抛出WrongParamValueEx(“ [MethodName]输入参数1值错误{0} ......,您将需要捕获异常并将其写入您的应用日志
JohnJohnGa 2011年

3
@约翰。如果这实际上是错误的,那么您是对的。但是通常不是。而且,与其检查每个被称为方法的地方,不如检查方法,而只是检查并返回无所作为。
霹雳霹雳州

3
我不愿读一个由两个屏幕代码,句点,ifs或no组成的方法。大声笑
jpmc26

1
正是由于这个原因,我个人更喜欢早期回报(另请避免嵌套)。但是,这与有关性能的原始问题无关,可能应该发表评论。
帕特里克M

22

它不仅影响美观,而且还防止代码嵌套。

实际上,它可以作为确保您的数据也有效的前提。


18

这当然是主观的,但是我认为它在两点上有很大的改进:

  • 现在很明显,如果condition保持有效,您的函数无事可做。
  • 它使嵌套级别降低。嵌套对可读性的影响比您想象的要大。

15

多个返回点是C语言(在较小程度上是C ++)中的一个问题,因为它们迫使您在每个返回点之前复制清除代码。使用垃圾收集,try| finally构造和using块,实际上没有理由为什么要害怕它们。

归根结底,这取决于您和您的同事更容易阅读。


那不是唯一的原因。从学术上讲,引用伪代码与诸如清洗东西之类的实际考虑无关。这种自相矛盾的理由与尊重命令式构造的基本形式有关。不用将循环出口放在中间。这样,可以严格检测不变量并证明行为。或可以证明终止。
v.oddou 2014年

1
新闻:实际上,我发现了一个非常实际的原因,为什么早期的休息和回报不好,这是我所说的静态分析的直接结果。您可以使用intel C ++编译器使用指南纸:d3f8ykwhia686p.cloudfront.net/1live/intel/…。关键词:a loop that is not vectorizable due to a second data-dependent exit
v.oddou 2014年

12

在性能方面,两种方法之间没有明显的区别。

但是编码不仅仅是性能。清晰度和可维护性也非常重要。而且,在不影响性能的情况下,这是唯一重要的事情。

关于哪种方法更可取,存在着相互竞争的思想流派。

一种观点是另一种观点:第二种方法降低了嵌套级别,从而提高了代码清晰度。这是命令式的自然现象:当您无事可做时,您最好早点回来。

从一种更实用的样式的角度来看,另一种观点是一种方法应该只有一个出口点。功能语言中的所有内容都是一种表达。因此,if语句必须始终具有else子句。否则,if表达式并不总是具有值。所以在功能风格上,第一种方法更自然。


11

Guard子句或前置条件(您可能会看到)进行检查以查看是否满足特定条件,然后中断程序流程。它们非常适合您仅对if声明的一个结果感兴趣的地方。因此,与其说:

if (something) {
    // a lot of indented code
}

您逆转条件,并在满足逆转条件时中断

if (!something) return false; // or another value to show your other code the function did not execute

// all the code from before, save a lot of tabs

return远不及肮脏goto。它允许您传递一个值,以显示该函数无法运行的其余代码。

您将看到在嵌套条件下可以应用的最佳示例:

if (something) {
    do-something();
    if (something-else) {
        do-another-thing();
    } else {
        do-something-else();
    }
}

vs:

if (!something) return;
do-something();

if (!something-else) return do-something-else();
do-another-thing();

您会发现很少有人争辩说第一个是更干净的,但当然,它是完全主观的。一些程序员喜欢通过缩进来了解某事物在什么条件下运行,而我宁愿保持方法流程线性。

我暂时不会建议先验会改变您的生活或使您被打败,但您可能会发现代码更容易阅读。


6
我想我是当时的少数几个。我发现阅读第一个版本比较容易。嵌套的if使决策树更加明显。另一方面,如果有几个前提条件,我同意最好将它们全部放在函数的顶部。
阿瑟赛德

@Otherside:完全同意。以串行方式编写的收益使您的大脑需要序列化可能的路径。例如,当if-tree可以直接映射到某些瞬时逻辑时,可以将其编译为lisp。但是串行返回方式将要求编译器更加困难。这里的观点显然与这种假设情况无关,它与代码分析,优化的机会,更正证明,终止证明和不变性检测有关。
v.oddou

1
没有人会发现更容易读取带有更多嵌套的代码。像某样东西的块越多,一眼就越容易阅读。在这里,第一个示例不可能更容易阅读,并且当现实世界中的大多数此类代码更深入时,只有两个嵌套,每个嵌套都需要更多的脑力才能跟随。
user441521

1
如果嵌套的深度是原来的两倍,那么您将花多长时间回答以下问题:“您什么时候“不做某事”?我想如果没有笔和纸就很难回答。但是,如果所有内容都经过严格规定,您可以轻松回答。
David Storfer

9

这里有几个优点,但是,如果方法很冗长,那么多个返回点也可能不可读。话虽如此,如果您要使用多个返回点,只需确保您的方法简短即可,否则可能会丢失多个返回点的可读性。


8

性能分为两个部分。在软件投入生产时您具有性能,但是在开发和调试时也要具有性能。开发人员想要的最后一件事就是“等待”一些琐碎的事情。最后,在启用优化的情况下进行编译将得到类似的代码。因此,很高兴知道这些在两种情况下都能奏效的小技巧。

问题中的情况很清楚,ReSharper是正确的。if在方法开始时,您要设置一条明确的规则,而不是嵌套语句并在代码中创建新的作用域。它提高了可读性,易于维护,并且减少了人们必须筛选以查找他们想要去的规则的数量。


7

就个人而言,我只喜欢1个出口点。如果您的方法简短而切题,这很容易实现,并且为下一个处理代码的人提供了可预测的模式。

例如。

 bool PerformDefaultOperation()
 {
      bool succeeded = false;

      DataStructure defaultParameters;
      if ((defaultParameters = this.GetApplicationDefaults()) != null)
      {
           succeeded = this.DoSomething(defaultParameters);
      }

      return succeeded;
 }

如果您只想在函数退出之前检查函数中某些局部变量的值,这也将非常有用。您需要做的就是在最终收益上放置一个断点,并保证可以达到目标(除非抛出异常)。


4
bool PerformDefaultOperation(){DataStructure defaultParameters = this.GetApplicationDefaults(); return(defaultParameters!= NULL && this.DoSomething(defaultParameters);},为您修复。:)
tchen

1
这个例子是非常基本的,并且错过了这种模式的要点。在此函数中还有大约4个其他检查,然后告诉我们它更具可读性,因为事实并非如此。
user441521 '16

5

关于代码看起来的很多原因。但是结果如何呢?

让我们看一些C#代码及其IL编译形式:


using System;

public class Test {
    public static void Main(string[] args) {
        if (args.Length == 0) return;
        if ((args.Length+2)/3 == 5) return;
        Console.WriteLine("hey!!!");
    }
}

这个简单的代码片段可以被编译。您可以使用ildasm打开生成的.exe文件,并检查结果。我不会发布所有汇编程序,但是会描述结果。

生成的IL代码执行以下操作:

  1. 如果第一个条件为假,则跳至第二个条件所在的代码。
  2. 如果是真的,则跳转到最后一条指令。(注意:最后一条指令是返回值)。
  3. 在第二种情况下,计算结果后也会发生同样的情况。比较:如果为false,则返回Console.WriteLine;如果为true,则返回结尾。
  4. 打印消息并返回。

因此,似乎代码将跳到最后。如果我们对嵌套代码进行常规处理会怎样?

using System;

public class Test {
    public static void Main(string[] args) {
        if (args.Length != 0 && (args.Length+2)/3 != 5) 
        {
            Console.WriteLine("hey!!!");
        }
    }
}

IL指令中的结果非常相似。不同之处在于,在每种情况下都没有跳转:如果为false,则转到下一段代码;如果为true,则转到然后结束。现在,IL代码的流动性更好,并具有3次跳转(编译器对此进行了一些优化):1.第一个跳转:当Length为0时,代码再次跳转(第三次跳转)到末尾。2.第二:在第二条件的中间避免一条指令。3.第三:如果第二个条件为假,则跳到最后。

无论如何,程序计数器将始终跳转。


5
这是个很好的信息-但我个人并不关心IL是否“流动得更好”,因为要管理代码的人不会看到任何IL
JohnIdol

1
与您今天看到的情况相比,您真的无法预期在C#编译器的下一个更新中优化将如何起作用。我个人不会花任何时间来调整C#代码以在IL中产生不同的结果,除非测试显示您在某个紧密循环中存在严重的性能问题,并且您已经用尽了其他想法。即使那样,我也会更努力地考虑其他选择。同样,我怀疑您是否知道JIT编译器会对每种平台的IL进行什么样的优化……
Craig 2013年

5

从理论上讲,如果反相if可以提高分支预测的命中率,则可以提高性能。在实践中,我认为很难确切地知道分支预测的行为方式,尤其是在编译之后,因此,除非我正在编写汇编代码,否则在日常开发中我不会这么做。

更多关于分支预测的信息


4

那简直是有争议的。在提早返还问题上没有“程序员之间的协议”。据我所知,它总是主观的。

可以提出一个性能参数,因为最好写一些条件使它们通常是真实的。也可以说这很清楚。另一方面,它确实会创建嵌套测试。

我认为您不会对此问题有任何定论的答案。


我不明白您的表现论点。条件是布尔值,所以无论结果如何,总会有两个结果...我不明白为什么反转(或不反转)一条语句会改变结果。(除非您说要在条件中添加“ NOT”,否则会增加可衡量的处理量...
Oli

2
优化的工作方式如下:如果确保最常见的情况是在if块而不是else块中,那么CPU通常已经在其管道中加载了if块中的语句。如果条件返回false时,CPU需要清空其管道和...
阿瑟赛德

我也喜欢做其他事情只是为了提高可读性,我讨厌“ then”块而不是“ else”中的否定格。即使不知道或不考虑CPU在做什么,也可以这样做
Andrew Bullock

3

已经有很多有见地的答案,但是,我还是要针对一种略有不同的情况:除了先决条件,它实际上应该放在函数的顶部,想一想分步初始化,在这里您可以必须检查每个步骤是否成功,然后继续下一步。在这种情况下,您无法检查顶部的所有内容。

我发现使用Steinberg的ASIOSDK编写ASIO主机应用程序时,我的代码确实不可读,因为我遵循嵌套范例。正如上面的安德鲁·布洛克(Andrew Bullock)所提到的,它的深度达到了八个层次,而且我看不到那里的设计缺陷。当然,我可以将一些内部代码打包到另一个函数中,然后在其中嵌套其余级别以使其更具可读性,但这对我来说似乎是随机的。

通过用保护子句代替嵌套,我什至发现了我的误解,认为这部分清除代码本应在函数内部而不是末尾发生。有了嵌套的分支,我将永远不会看到,甚至可以说它们导致了我的误解。

因此,这可能是另一种情况,即ifs可能有助于编写更清晰的代码。


3

避免多个出口点可以提高性能。我不确定C#,但是在C ++中,命名返回值优化(Copy Elision,ISO C ++ '03 12.8 / 15)取决于单个出口点。这种优化避免了复制构造您的返回值(在您的特定示例中,这无关紧要)。这可能会导致紧密循环中的性能显着提高,因为每次调用该函数时都保存了一个构造函数和一个析构函数。

但是对于99%的情况,保存额外的构造函数和析构函数调用并不值得,因为嵌套if块引入了可读性(正如其他人指出的那样)。


2
那里。我花了3个长读,用3个问题问了同样的问题(其中2个被冻结),得出了数十个答案,最终找到了一个提到NRVO的人。哎呀...谢谢
v.oddou

2

这是一个见解的问题。

我通常的方法是避免单行ifs,而在方法中间返回。

您不会希望这样的行在您的方法中的任何地方都显示出来,但是要说一遍,要检查方法顶部的一堆假设,并且只有在所有条件都通过时才做您的实际工作。


2

在我看来,如果您只是返回void(或者您永远不会检查的一些无用的返回代码),那么早返回是很好的选择,它可能会提高可读性,因为避免了嵌套,同时您明确声明了函数已完成。

如果您实际上返回的是returnValue-嵌套通常是一种更好的方法,因为您将returnValue放在一个位置(最后-duh),这可能会使代码在很多情况下更易于维护。


1

我不确定,但是我认为R#会尝试避免跳远。当您拥有IF-ELSE时,编译器将执行以下操作:

条件为假->跳转至false_condition_label

true_condition_label:指令1 ...指令n

false_condition_label:指令1 ...指令n

端块

如果条件为true,则不会进行跳转,也不会部署L1高速缓存,但是跳转到false_condition_label可能会很遥远,处理器必须推出自己的高速缓存。同步缓存非常昂贵。R#尝试将远距离跳转替换为短距离跳转,在这种情况下,所有指令都已经在缓存中的可能性更大。


0

我认为这取决于您的喜好,如前所述,没有普遍的协议。为了减少烦恼,您可以将此类警告减少为“提示”


0

我的想法是,“在函数中间”的返回值不应如此“主观”。原因很简单,采用以下代码:

    函数do_something(data){

      如果(!is_valid_data(data)) 
            返回false;


       do_something_that_take_an_hour(data);

       istance =新的object_with_very_painful_constructor(data);

          如果(距离无效){
               错误信息( );
                回报;

          }
       connect_to_database();
       get_some_other_data();
       返回;
    }

也许第一个“返回”并不是那么直观,但这确实可以节省。关于干净代码的“想法”太多了,它们只需要更多的实践就可以失去他们的“主观”坏主意。


使用异常语言,您不会遇到这些问题。
user441521

0

这种编码有几个优点,但对我来说,最大的好处是,如果您可以快速返回,则可以提高应用程序的速度。IE,我知道由于前提X,我可以快速返回错误。这首先消除了错误情况,并降低了代码的复杂性。在许多情况下,由于现在可以更清洁cpu管道,因此可以停止管道崩溃或切换。其次,如果您处于循环中,快速中断或退出可以节省大量CPU。一些程序员使用循环不变式来进行这种快速退出,但是在这种情况下,您可能会破坏您的cpu管道,甚至造成内存查找问题,这意味着cpu需要从外部缓存加载。但基本上我认为您应该按照自己的意愿去做,也就是说,循环或函数不会创建复杂的代码路径,而只是为了实现正确代码的某些抽象概念。如果您拥有的唯一工具是锤子,那么一切看起来都像钉子。


读了graffic的答案,听起来听起来像是编译器优化了嵌套代码,使执行的代码比使用多次返回更快。但是,即使更快地获得多个回报,此优化也可能不会加快您的应用程序的速度……:)
hangy

1
我不同意-第一定律是关于使算法正确。但是我们都是工程师,这是关于使用我们的资源来解决正确的问题,并在可用的预算范围内进行。太慢的应用程序不适合目标。
David Allan Finch,
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.