反转"if"语句以减少嵌套

Lea*_*hen 257 c# resharper

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

    if (some condition)
    {
        Some code...            
    }
Run Code Online (Sandbox Code Playgroud)

ReSharper给了我上面的警告(反转"if"声明以减少嵌套),并提出了以下更正:

   if (!some condition) return;
   Some code...
Run Code Online (Sandbox Code Playgroud)

我想明白为什么那样更好.我一直认为在方法中间使用"返回"有问题,有点像"goto".

Jon*_*Jon 329

它不仅美观,而且还降低了方法内的最大嵌套水平.这通常被认为是一个优点,因为它使方法更容易理解(实际上,许多 静态 分析 工具提供了一种衡量代码质量指标的方法).

另一方面,它也使你的方法有多个退出点,这是另一群人认为是禁止的.

就我个人而言,我同意ReSharper和第一组(用一种例外的语言我发现讨论"多个退出点"很愚蠢;几乎任何东西都可以抛出,因此所有方法都有很多潜在的退出点).

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

  • 单点退出?[谁需要它?(http://programmers.stackexchange.com/questions/118703/where-did-the-notion-of-one-return-only-come-from) (40认同)
  • @ sq33G:关于SESE的问题(以及当然的答案)非常棒.谢谢你的链接! (3认同)

jop*_*jop 282

方法中间的返回不一定是坏的.如果它使代码的意图更清晰,那么立即返回可能会更好.例如:

double getPayAmount() {
    double result;
    if (_isDead) result = deadAmount();
    else {
        if (_isSeparated) result = separatedAmount();
        else {
            if (_isRetired) result = retiredAmount();
            else result = normalPayAmount();
        };
    }
     return result;
};
Run Code Online (Sandbox Code Playgroud)

在这种情况下,如果_isDead是,我们可以立即退出该方法.以这种方式构造它可能更好:

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

    return normalPayAmount();
};   
Run Code Online (Sandbox Code Playgroud)

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

  • 可能只是一个品味问题:我建议将第二和第三个"if"更改为"else if"以进一步提高可读性.如果忽略"返回"声明,则仍然清楚的是,仅检查以下情况是否前一个失败,即检查的顺序是重要的. (14认同)
  • 这是一个非常好的例子!重构的代码更像是一个case语句. (13认同)
  • 在一个例子中,这个简单,id同意第二种方式更好,但只是因为它显然发生了什么. (2认同)

Leo*_*Hat 101

这有点宗教争论,但我同意ReSharper的观点,你应该更喜欢筑巢.我相信这超过了从函数中获得多个返回路径的负面影响.

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

前提条件是在函数开始时可以提前返回的一个很好的例子.为什么函数的其余部分的可读性会受到前置条件检查的影响?

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

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

代码可读性差.

  • 我在开口支架上放了一个断点.如果您单步执行该功能,您不仅可以看到按顺序执行的检查,而且还可以非常清楚地检查该功能是否为该运行进行了检查. (10认同)
  • @Nailer很晚,我特别同意,如果功能太大而无法通过,那么应该将其分成多个功能! (5认同)
  • 在这里同意约翰......我没有看到只是单步执行整个功能的问题. (3认同)
  • 函数中的多次返回确实会影响维护程序员。在调试函数时,寻找流氓返回值,维护者必须在所有可以返回的地方放置断点。 (2认同)

Mic*_*wan 69

正如其他人所提到的,不应该有性能损失,但还有其他考虑因素.除了那些有效的顾虑之外,在某些情况下,这也可能会让你陷入困境.假设你正在处理一个double代替:

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

看似等效的反演形成鲜明对比:

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

因此,在某些情况下,似乎是正确倒置的if可能不是.

  • 还应该注意的是,resharper快速修复将exampleParam> 0反转为exampleParam <0*而不是*exampleParam <= 0这引起了我的注意. (4认同)

Sco*_*ham 50

仅在函数结束时返回的想法从语言支持异常之前的几天开始回归.它使程序能够依赖于能够在方法结束时放置清理代码,然后确保它将被调用,并且其他程序员不会隐藏导致清理代码被跳过的方法中的返回.跳过的清理代码可能导致内存或资源泄漏.

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

无论如何,我说我认为很多人引用"方法结束时的唯一回报"指南而不理解为什么这是一件好事,并且减少嵌套以提高可读性可能是一个更好的目标.

  • @Eric你必须暴露在异常糟糕的代码中.当它们被错误地使用时很明显,它们通常允许您编写更高质量的代码. (16认同)
  • 你只是想出了为什么例外是UglyAndEvil [tm] ... ;-)例外是花哨的,昂贵的伪装. (5认同)

Pio*_*rak 27

我想补充一点,那些倒置的名字是 - Guard Clause.我尽可能地使用它.

我讨厌阅读代码,如果在开头有两个代码屏幕而没有别的.只需反转if并返回.这样,没有人会浪费时间滚动.

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

  • @约翰.如果这实际上是错误,那你就是对的.但通常情况并非如此.而不是在调用方法的每个地方检查某些东西,方法只是检查它并返回什么都不做. (3认同)
  • 我不愿意阅读一个方法,它是两个代码屏幕,句号,`if`s或no.大声笑 (3认同)
  • 确切地。它更像是一种重构。正如您提到的,它有助于更​​轻松地阅读代码。 (2认同)

Rio*_*ams 21

它不仅影响美学,还会阻止代码嵌套.

它实际上可以作为确保数据有效的前提条件.


Dee*_*tan 18

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

  • 现在很明显,如果condition保持,你的功能还有什么可做的.
  • 它保持嵌套水平.嵌套会比您想象的更容易损害可读性.


Ric*_*ole 14

多个返回点是C中的问题(在较小程度上是C++),因为它们强制您在每个返回点之前复制清理代码.垃圾收集,try| finally构建和using阻止,你真的没有理由害怕它们.

归根结底,它归结为您和您的同事更容易阅读的内容.


Jef*_*Sax 12

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

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

关于哪种方法更可取,有各种各样的思想流派.

一种观点是其他人提到的观点:第二种方法降低了嵌套级别,从而提高了代码清晰度.这在命令式的风格中是很自然的:当你没有什么可做的时候,你也可以尽早回归.

从功能性更强的角度来看,另一种观点是方法应该只有一个出口点.功能语言中的一切都是表达.所以如果语句必须总是有一个else子句.否则if表达式不会总是有值.所以在功能风格上,第一种方法更自然.


Oli*_*Oli 11

保护条款或前提条件(您可能会看到)检查是否满足某个条件,然后打破程序流程.它们非常适合那些你真正只对一个if陈述的结果感兴趣的地方.所以不要说:

if (something) {
    // a lot of indented code
}
Run Code Online (Sandbox Code Playgroud)

如果满足相反的条件,您可以反转条件并中断

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
Run Code Online (Sandbox Code Playgroud)

return远不如肮脏goto.它允许您传递一个值来显示函数无法运行的其余代码.

您将看到可以在嵌套条件中应用此位置的最佳示例:

if (something) {
    do-something();
    if (something-else) {
        do-another-thing();
    } else {
        do-something-else();
    }
}
Run Code Online (Sandbox Code Playgroud)

VS:

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

if (!something-else) return do-something-else();
do-another-thing();
Run Code Online (Sandbox Code Playgroud)

你会发现很少有人认为第一个是更清洁,但当然,这是完全主观的.一些程序员喜欢知道什么条件是通过缩进操作的,而我宁愿保持方法流线性.

我暂时不会建议precons会改变你的生活或让你得到你的生活,但你可能会发现你的代码更容易阅读.

  • 我想我是少数几个之一.我发现阅读第一个版本更容易.嵌套的ifs使决策树更加明显.另一方面,如果有一些前提条件,我同意最好将它们全部放在函数的顶部. (6认同)

Jon*_*jap 9

这里有几个好点,但如果方法非常冗长,多个返回点也是不可读的.话虽如此,如果您要使用多个返回点,请确保您的方法很短,否则多个返回点的可读性加值可能会丢失.


Rya*_*ier 8

表演分为两部分.当软件处于生产阶段时,您可以获得性能,但您也希望在开发和调试时获得性能.开发人员想要的最后一件事是"等待"一些微不足道的事情.最后,通过启用优化来编译它将导致类似的代码.因此,了解这些在两种情况下都能获得回报的小技巧是很好的.

问题的案例很清楚,ReSharper是正确的.if您可以在方法的开头设置一个明确的规则,而不是嵌套语句,并在代码中创建新的范围.它增加了可读性,更易于维护,并且减少了必须筛选以找到他们想要去的地方的规则数量.


ili*_*rit 7

我个人更喜欢只有1个出口点.如果你保持方法的简洁和重点,它很容易实现,它为下一个处理代码的人提供了一个可预测的模式.

例如.

 bool PerformDefaultOperation()
 {
      bool succeeded = false;

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

      return succeeded;
 }
Run Code Online (Sandbox Code Playgroud)

如果您只想在函数退出之前检查函数中某些局部变量的值,这也非常有用.您需要做的就是在最终返回时放置一个断点,并保证您可以点击它(除非抛出异常).

  • bool PerformDefaultOperation(){DataStructure defaultParameters = this.GetApplicationDefaults(); return(defaultParameters!= NULL && this.DoSomething(defaultParameters);}在那里,为你修复它.:) (4认同)
  • 这个例子非常基础,没有抓住这个模式的要点。在此函数内进行大约 4 个其他检查,然后告诉我们它更具可读性,因为事实并非如此。 (2认同)

shi*_*umi 6

避免多个退出点可以带来性能提升。我不确定 C#,但在 C++ 中,命名返回值优化(复制消除,ISO C++ '03 12.8/15)取决于是否有一个出口点。这种优化避免了复制构造您的返回值(在您的具体示例中这并不重要)。这可能会在紧密循环中显着提高性能,因为每次调用函数时都会保存构造函数和析构函数。

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

  • 那里。我花了三遍时间阅读了三个问题中的数十个答案(其中两个问题被冻结),最后才找到有人提到 NRVO。哎呀...谢谢你。 (3认同)

gra*_*fic 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. 如果第一个条件为false,则跳转到第二个条件的代码.
  2. 如果它是真的跳转到最后一条指令.(注意:最后一条指令是退货).
  3. 在第二个条件下,在计算结果后也会发生相同的情况.比较和:如果为false则转到Console.WriteLine,如果是,则转到最后.
  4. 打印邮件并返回.

所以似乎代码将跳到最后.如果我们使用嵌套代码执行常规操作会怎么样?

using System;

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

IL指令的结果非常相似.区别在于每个条件跳转之前:如果为false则转到下一段代码,如果为true则转到结束.现在IL代码更好地流动并且有3次跳转(编译器对此进行了优化):1.第一次跳转:当Length为0时,代码再次跳转(第三次跳转)到结束.2.第二:在第二个条件中间避免一条指令.3.第三:如果第二个条件是假的,跳到最后.

无论如何,程序计数器总是会跳转.

  • 这是一个很好的信息 - 但个人我不在乎IL"流动得更好",因为那些管理代码的人不会看到任何IL (5认同)
  • 与您今天看到的相比,您确实无法预测 C# 编译器下一次更新中的优化过程将如何发挥作用。就我个人而言,我不会花任何时间尝试调整 C# 代码以便在 IL 中产生不同的结果,除非测试表明您在某个紧密循环中存在严重的性能问题并且您已经没有其他想法了。即便如此,我也会更仔细地考虑其他选择。同样,我怀疑您是否知道 JIT 编译器对每个平台提供给它的 IL 进行了哪些优化...... (2认同)

jfg*_*956 5

理论上,if如果提高分支预测命中率,反转可能会带来更好的性能。在实践中,我认为很难确切知道分支预测的行为,尤其是在编译之后,所以我不会在日常开发中这样做,除非我正在编写汇编代码。

更多关于这里的分支预测。