代码之家  ›  专栏  ›  技术社区  ›  Lea Cohen mark winkle

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

  •  326
  • Lea Cohen mark winkle  · 技术社区  · 17 年前

    当我奔跑时 ReSharper 例如,在我的代码中:

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

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

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

    我想知道为什么这样更好。我一直认为在方法中间使用“return”是有问题的,有点像“goto”。

    25 回复  |  直到 12 年前
        1
  •  347
  •   hdoghmen Neil Knight    11 年前

    这不仅是美学 ,但也减少了 maximum nesting level 在方法内部。这通常被认为是一个优点,因为它使方法更容易理解(事实上, many static analysis tools 提供对此的度量作为代码质量的指标之一)。

    另一方面,它也使你的方法有多个退出点,而另一组人认为这是不允许的。

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

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

        2
  •  320
  •   Joshua Pinter    11 年前

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

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

    在这种情况下,如果 _isDead 如果是真的,我们可以立即摆脱这种方法。这样构建可能会更好:

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

    我从 refactoring catalog 。这种特定的重构称为:用保护子句替换嵌套条件语句。

        3
  •  104
  •   Peter Mortensen Pieter Jan Bonestroo    12 年前

    这有点宗教色彩,但我同意ReSharper的观点,即你应该更喜欢少筑巢。我认为这超过了函数有多个返回路径的负面影响。

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

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

    至于从一个方法返回多次的负面影响,调试器现在非常强大,很容易准确地找出特定函数返回的位置和时间。

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

    代码可读性差。

        4
  •  70
  •   Michael McGowan    14 年前

    正如其他人所提到的,不应该影响性能,但还有其他考虑因素。除了这些合理的担忧之外,在某些情况下,这也会让你陷入困境。假设你正在处理一个 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)
    }
    

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

        5
  •  54
  •   Scott Langham    12 年前

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

    然而,在支持例外的语言中,它不提供这样的保证。在支持异常的语言中,执行任何语句或表达式都可能导致控制流,从而导致方法结束。这意味着必须通过使用finally或使用关键字来进行清理。

    不管怎样,我认为很多人引用“方法末尾唯一返回”的指导方针,而不理解为什么这是一件好事,减少嵌套以提高可读性可能是一个更好的目标。

        6
  •  34
  •   Piotr Perak    14 年前

    我想补充一点,那些倒置的if是有名字的——保护条款。只要有可能,我就用它。

    我讨厌在一开始就有if的地方阅读代码,只有两个屏幕的代码,没有其他屏幕。只需反转if并返回。这样就没有人会浪费时间滚动。

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

        7
  •  22
  •   Rion Williams    12 年前

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

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

        8
  •  18
  •   Deestan    17 年前

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

    • 现在很明显,如果 condition 持有。
    • 它可以降低筑巢水平。嵌套对可读性的伤害比你想象的要大。
        9
  •  15
  •   Richard Poole MatzFan    17 年前

    多个返回点在C中是一个问题(在较小程度上是C++),因为它们迫使你在每个返回点之前复制清理代码。通过垃圾回收 try | finally 建设和 using 积木,你真的没有理由害怕它们。

    归根结底,这取决于你和你的同事觉得什么更容易阅读。

        10
  •  12
  •   Oli    17 年前

    保护条款或先决条件(如您可能看到的)检查是否满足某个条件,然后中断程序的运行。它们非常适合那些你只对一个结果感兴趣的地方 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();
    

    你会发现很少有人认为第一种更干净,但当然,这完全是主观的。一些程序员喜欢通过缩进来了解某个东西在什么条件下运行,而我更愿意保持方法流的线性。

    我一点也不建议预编码会改变你的生活或让你上床,但你可能会发现你的代码更容易阅读。

        11
  •  12
  •   Jeffrey Sax    14 年前

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

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

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

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

    从功能性风格的角度来看,另一种观点是一个方法应该只有一个出口点。函数式语言中的一切都是表达式。因此,if语句必须始终包含else子句。否则if表达式并不总是有值。因此,在功能风格中,第一种方法更自然。

        12
  •  9
  •   Jon Limjap    17 年前

    这里有几个好点,但有多个回报点 可能无法阅读 如果方法很长。也就是说,如果你要使用多个返回点,只要确保你的方法很短,否则多个返回点将失去可读性。

        13
  •  9
  •   Peter Mortensen Pieter Jan Bonestroo    14 年前

    性能分为两部分。当软件在生产环境中时,您有性能,但您也希望在开发和调试时有性能。开发人员最不希望的就是“等待”一些琐碎的事情。最后,在启用优化的情况下编译它将产生类似的代码。所以很高兴知道这些小技巧在这两种情况下都有回报。

    问题中的情况很明显,ReSharper是正确的。而不是筑巢 if 语句,并在代码中创建新的作用域,您可以在方法的开头设置一个明确的规则。它增加了可读性,更容易维护,并减少了人们必须筛选的规则数量,以找到他们想去的地方。

        14
  •  7
  •   ilitirit    17 年前

    就我个人而言,我更喜欢只有一个出口点。如果你保持你的方法简短扼要,这很容易实现,而且它为下一个处理你代码的人提供了一个可预测的模式。

    如。

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

    如果你只是想在函数退出之前检查函数内某些局部变量的值,这也是非常有用的。你所需要做的就是在最终返回上放置一个断点,你保证会命中它(除非抛出异常)。

        15
  •  6
  •   Nikita    5 年前

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

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

        16
  •  5
  •   nevyn jfg956    8 年前

    有很多很好的理由 代码的外观 但是呢 结果 ?

    让我们来看看一些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!!!");
        }
    }
    

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

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

    1. 如果第一个条件是 false ,跳到第二个位置的代码。
    2. 如果是 true 跳到最后一条指令。(注意:最后一条指令是返回)。
    3. 在第二种情况下,计算结果后也会发生同样的情况。比较并:到达 Console.WriteLine 如果 错误的 或者,如果这是 符合事实的 .
    4. 打印消息并返回。

    因此,代码似乎会跳到末尾。如果我们用嵌套代码做一个普通的if怎么办?

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

    IL指令中的结果非常相似。不同之处在于,之前每个条件都有两次跳跃:如果 错误的 转到下一段代码,如果 符合事实的 走到最后。现在IL代码流得更好,有3个跳转(编译器对此进行了一些优化):

    1. 第一次跳转:当长度为0时,代码再次跳转(第三次跳转)到末尾。
    2. 第二:在中间的第二个条件,以避免一个指令。
    3. 第三:如果第二个条件是 错误的 ,跳到最后。

    不管怎样,程序计数器总是会跳起来。

        17
  •  4
  •   unwind    17 年前

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

    更多关于分支预测的信息 here .

        18
  •  4
  •   shibumi    14 年前

    这完全是有争议的。在提前返回的问题上没有“程序员之间的共识”。据我所知,这总是主观的。

    可以进行性能论证,因为最好写下条件,这样它们最常为真;也可以说,它更清晰。另一方面,它确实创建了嵌套测试。

    我认为你不会得到这个问题的确切答案。

        19
  •  3
  •   Ingo Schalk-Schupp    15 年前

    这里已经有很多有见地的答案了,但我还是想指出一个稍微不同的情况:与其先决条件,不如把它放在函数之上——事实上,想想一个循序渐进的初始化过程,你必须检查每一步是否成功,然后继续下一步。在这种情况下,您无法检查顶部的所有内容。

    当我使用Steinberg的ASIOSDK编写ASIO主机应用程序时,我发现我的代码真的很难阅读,因为我遵循了嵌套范式。它有八层深,我看不出有任何设计缺陷,正如Andrew Bullock上面提到的那样。当然,我本可以将一些内部代码打包到另一个函数中,然后将剩余的级别嵌套在那里,使其更具可读性,但这对我来说似乎相当随机。

    通过用保护子句替换嵌套,我甚至发现了我对清理代码的一部分的误解,这部分代码本应在函数中更早发生,而不是在末尾。使用嵌套分支,我永远不会看到这种情况,你甚至可以说它们导致了我的误解。

    因此,这可能是另一种情况,在这种情况下,倒排ifs可以使代码更清晰。

        20
  •  2
  •   Colin Pickard    17 年前

    这是一个意见问题。

    我的正常方法是避免单行if,并在方法的中间返回。

    你不希望在你的方法中到处都有像它所暗示的那样的行,但有一点值得一提的是,要检查方法顶部的一堆假设,只有当它们都通过时,才能进行实际工作。

        21
  •  2
  •   JohnIdol    17 年前

    在我看来,如果你只是返回void(或者一些你永远不会检查的无用返回代码),提前返回是可以的,这可能会提高可读性,因为你避免了嵌套,同时你明确表示你的函数已经完成。

    如果你真的要返回一个returnValue,嵌套通常是一种更好的方法,因为你只在一个地方返回returnValue(在末尾-duh),这可能会使你的代码在很多情况下更易于维护。

        22
  •  1
  •   Marcin Gołembiewski    7 年前

    我不确定,但我认为,R#试图避免远跳。当你有IF-ELSE时,编译器会做这样的事情:

    条件为false->远跳到false条件标签

    true_condition_label: 说明1 ... 说明_n

    false条件标签: 说明1 ... 说明_n

    端块

    若条件为真,则没有跳转,也没有卷展L1缓存,但跳转到false_condition_label可能很远,处理器必须卷展自己的缓存。同步缓存的成本很高。R#尝试将远跳转替换为短跳转,在这种情况下,所有指令都已在缓存中的可能性更大。

        23
  •  0
  •   Bluenuance    17 年前

    我认为这取决于你更喜欢什么,正如前文所述,似乎没有普遍的共识。 为了减少烦恼,您可以将这种警告减少为“提示”

        24
  •  0
  •   Joshi Spawnbrood    17 年前

    我的想法是,“函数中间”的返回不应该如此“主观”。 原因很简单,以这段代码为例:

        function do_something( data ){
    
          if (!is_valid_data( data )) 
                return false;
    
    
           do_something_that_take_an_hour( data );
    
           istance = new object_with_very_painful_constructor( data );
    
              if ( istance is not valid ) {
                   error_message( );
                    return ;
    
              }
           connect_to_database ( );
           get_some_other_data( );
           return;
        }
    

    也许第一次“回归”不是那么直观,但这真的很省钱。 关于干净代码的“想法”太多了,只需要更多的练习就可以摆脱“主观”的坏想法。

        25
  •  0
  •   David Allan Finch    17 年前

    这种编码有几个优点,但对我来说,最大的好处是,如果你能快速返回,你可以提高应用程序的速度。IE我知道,由于前提条件X,我可以快速返回错误。这首先消除了错误情况,降低了代码的复杂性。在很多情况下,由于cpu管道现在可以更干净,它可以阻止管道崩溃或切换。其次,如果你处于循环中,快速中断或退出可以为你节省大量的cpu。一些程序员使用循环不变量来实现这种快速退出,但在这种情况下,你可能会破坏你的cpu管道,甚至造成内存查找问题,这意味着cpu需要从外部缓存加载。但基本上,我认为你应该做你想做的事情,即结束循环或函数,而不是仅仅为了实现正确代码的抽象概念而创建复杂的代码路径。如果你唯一的工具是锤子,那么一切看起来都像钉子。