302

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

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

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

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

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

4

25 回答 25

349

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

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

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

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

于 2011-12-08T20:34:26.280 回答
321

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

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

我从重构目录中挑选了这段代码。这种特定的重构称为:用保护子句替换嵌套条件。

于 2008-11-06T10:11:23.747 回答
104

这有点宗教争论,但我同意 ReSharper 的观点,即您应该更喜欢少嵌套。我相信这胜过从一个函数中拥有多个返回路径的负面影响。

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

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

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

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

代码可读性差会。

于 2008-11-06T10:01:21.960 回答
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)
}

因此,在某些情况下,看起来正确反转的 aaif可能不是。

于 2011-12-08T21:52:54.150 回答
54

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

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

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

于 2008-11-06T10:16:43.457 回答
34

我想补充一点,那些倒置的 if 有一个名字 - 保护条款。我尽可能使用它。

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

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

于 2011-12-13T17:12:41.393 回答
22

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

它实际上可以作为一个前提条件来确保您的数据也是有效的。

于 2011-12-08T20:36:05.670 回答
18

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

  • condition现在很明显,如果成立,您的函数将无事可做。
  • 它使嵌套级别降低。嵌套对可读性的伤害比你想象的要大。
于 2008-11-06T10:05:38.080 回答
15

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

最终归结为您和您的同事认为更容易阅读的内容。

于 2008-11-06T10:23:13.943 回答
12

保护子句或前置条件(如您可能看到的)检查是否满足某个条件,然后中断程序的流程。它们非常适合您真正只对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();
    }
}

与:

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

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

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

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

于 2008-11-06T10:07:18.390 回答
12

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

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

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

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

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

于 2011-12-08T20:54:24.147 回答
9

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

于 2008-11-06T10:27:57.083 回答
9

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

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

于 2011-12-08T21:29:15.103 回答
7

我个人更喜欢只有 1 个出口点。如果你的方法简明扼要,这很容易完成,并且它为下一个处理你的代码的人提供了一个可预测的模式。

例如。

 bool PerformDefaultOperation()
 {
      bool succeeded = false;

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

      return succeeded;
 }

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

于 2008-11-07T07:43:25.123 回答
6

关于代码外观的许多充分理由。但是结果呢?

让我们看一下一些 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.WriteLineiffalse或到 end if this is true
  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 指令中的结果非常相似。不同的是之前每个条件有两次跳转:如果false转到下一段代码,如果true转到最后。现在 IL 代码更好地流动并且有 3 次跳转(编译器对此做了一点优化):

  1. 第一次跳转:当Length为0时,代码再次跳转(第三次跳转)到最后的部分。
  2. 第二:在第二个条件中间避免一条指令。
  3. 第三:如果第二个条件是false,跳到最后。

无论如何,程序计数器会一直跳。

于 2008-11-06T11:11:38.057 回答
6

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

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

于 2011-12-14T17:33:13.287 回答
5

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

更多关于分支预测的信息

于 2011-12-24T12:39:35.773 回答
4

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

可以进行性能论证,因为最好将条件写成这样,以便它们通常是正确的;也可以说它更清楚。另一方面,它确实创建了嵌套测试。

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

于 2008-11-06T09:59:27.143 回答
3

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

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

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

所以这可能是另一种情况,倒置的 if 可以有助于更清晰的代码。

于 2011-06-14T11:41:48.900 回答
2

这是一个见仁见智的问题。

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

您不希望在您的方法中随处可见这样的行,但是要检查方法顶部的一堆假设,并且只有在它们都通过时才进行实际工作。

于 2008-11-06T10:00:17.453 回答
2

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

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

于 2008-11-06T11:05:49.753 回答
1

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

条件为假 -> 远跳转到 false_condition_label

true_condition_label:指令1 ...指令n

false_condition_label:指令1 ...指令n

结束块

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

于 2019-05-21T19:20:47.550 回答
0

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

于 2008-11-06T10:03:22.907 回答
0

我的想法是“在函数中间”的返回不应该那么“主观”。原因很简单,拿这段代码:

    函数do_something(数据){

      如果(!is_valid_data(数据))
            返回假;


       do_something_that_take_an_hour(数据);

       istance = new object_with_very_painful_constructor(data);

          if ( istance 无效) {
               错误信息( );
                返回 ;

          }
       连接到数据库();
       get_some_other_data();
       返回;
    }

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

于 2008-11-06T10:09:52.377 回答
0

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

于 2008-11-06T10:41:21.723 回答