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

时间:2008-11-06 09:55:27

标签: c# resharper

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

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

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

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

我想了解为什么那更好。我一直认为在方法中间使用“返回”有问题,有点像“goto”。

25 个答案:

答案 0 :(得分:326)

这不仅仅是审美,还会减少方法中的maximum nesting level。这通常被视为一个优点,因为它使方法更容易理解(事实上,many static analysis tools提供了一种衡量这一点的代码指标质量)。

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

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

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

答案 1 :(得分:267)

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

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中选择了此代码。这种特定的重构称为:使用Guard子句替换嵌套的条件。

答案 2 :(得分:100)

这是一个宗教争论,但我同意ReSharper你应该更喜欢筑巢。我认为这超过了从函数中获得多个返回路径的负面影响。

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

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

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

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

代码可读性差。

答案 3 :(得分:68)

正如其他人所说,不应该有性能损失,但还有其他考虑因素。除了那些有效的顾虑之外,在某些情况下,这也可能会让你陷入困境。假设您正在处理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可能不是。

答案 4 :(得分:50)

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

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

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

答案 5 :(得分:26)

我想补充一点,如果是倒卫的话还有名字 - 警卫条款。我随时都可以使用它。

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

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

答案 6 :(得分:21)

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

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

答案 7 :(得分:18)

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

  • 现在很明显,如果condition成立,您的功能将无需执行任何操作。
  • 它保持嵌套级别。嵌套会比你想象的更容易伤害可读性。

答案 8 :(得分:14)

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

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

答案 9 :(得分:12)

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

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

关于哪种方法更受欢迎,有各种各样的思想流派。

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

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

答案 10 :(得分:11)

保护条款或前提条件(正如您可能看到的那样)检查是否满足某个条件,然后打破程序的流程。它们非常适合那些你真正只对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();

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

我暂时不会建议precons会改变你的生活或让你安静下来,但你可能会发现你的代码更容易阅读。

答案 11 :(得分:9)

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

答案 12 :(得分:8)

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

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

答案 13 :(得分:7)

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

例如

 bool PerformDefaultOperation()
 {
      bool succeeded = false;

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

      return succeeded;
 }

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

答案 14 :(得分: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. 打印信息并返回。
  5. 所以似乎代码将跳到最后。如果我们使用嵌套代码执行常规操作会怎么样?

    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.第一次跳转:当长度为0时,代码再次跳转的部分(第三次跳转)到最后。   2.第二:在第二个条件中间避免一条指令。   3.第三:如果第二个条件是假的,跳到最后。

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

答案 15 :(得分:4)

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

有关分支预测的更多信息here

答案 16 :(得分:4)

这简直是有争议的。早期回归问题上没有“程序员之间的协议”。据我所知,它总是主观的。

有可能做一个性能论证,因为最好有条件写入,因此它们最常见;也可以说它更清楚。另一方面,它确实创建了嵌套测试。

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

答案 17 :(得分:3)

那里已经有很多有见地的答案了,但是,我仍然会指向一个稍微不同的情况:而不是前置条件,这应该放在一个函数之上,考虑一步一步的初始化,你必须检查每个步骤是否成功,然后继续下一步。在这种情况下,您无法检查顶部的所有内容。

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

通过用guard子句替换嵌套,我甚至发现了一个关于清理代码的错误概念,它应该在函数内部更早发生,而不是在最后。对于嵌套的分支,我永远不会看到它,你甚至可以说它们导致了我的误解。

所以这可能是另一种情况,其中反转ifs可以为更清晰的代码做出贡献。

答案 18 :(得分:3)

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

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

答案 19 :(得分:2)

这是一个意见问题。

我的常规方法是避免使用单行ifs,并在方法中间返回。

你不希望在你的方法中随处可见任何行,但是在方法的顶部检查一堆假设有一些东西可以说,只有在它们都通过时才做你的实际工作。

答案 20 :(得分:2)

在我看来,如果你只是返回void(或者你将永远无法检查的一些无用的返回代码),那么早期返回就没问题了,它可能会提高可读性,因为你避免了嵌套,同时你明确表示你的功能是完成。

如果你实际上正在返回一个returnValue - 嵌套通常是一种更好的方法,因为你只需要在一个地方(最后 - duh)返回你的returnValue,它可能会使你的代码在很多情况下更易于维护

答案 21 :(得分:0)

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

答案 22 :(得分:0)

我的想法是,“在功能中间”的回报不应该如此“主观”。 原因很简单,请使用以下代码:

    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;
    }

也许第一次“回归”并不是那么直观,但这真的很省钱。 关于清洁代码的“想法”太多,只需要更多的练习就可以失去“主观”的坏主意。

答案 23 :(得分:0)

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

答案 24 :(得分:0)

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

条件错误->跳转至false_condition_label

true_condition_label: 说明1 ... 说明_n

false_condition_label: 说明1 ... 说明_n

结束块

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