反“if"语句来减少嵌套

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

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

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

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

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

160323 次浏览

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

这是有可能的,因为最好有条件,所以它们通常是真的;也可以说,它更清晰。另一方面,它确实创建了嵌套测试。

我不认为你会对这个问题得到一个结论性的答案。

这是意见的问题。

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

你不希望在你的方法中到处都是这样的行,但是在你的方法顶部检查一堆假设是有意义的,只有在它们都通过时才做实际的工作。

这是一个有点宗教的争论,但我同意ReSharper的观点,你应该更喜欢少嵌套。我相信这超过了一个函数有多条返回路径的缺点。

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

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

至于从一个方法返回多次的缺点——现在调试器非常强大,很容易找到特定函数返回的确切位置和时间。

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

糟糕的代码可读性会。

我认为这取决于你喜欢什么,如前所述,没有统一的意见。 为了减少麻烦,您可以将这种警告减少为"Hint"

这当然是主观的,但我认为它极大地改善了两点:

  • 现在很明显,如果condition存在,你的函数就没有什么可做的了。
  • 它保持嵌套级别较低。嵌套对可读性的伤害比你想象的要大。

保护子句或先决条件(正如您可能看到的那样)检查是否满足某个条件,然后中断程序的流程。它们适用于你只对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会改变你的生活或让你上床,但你可能会发现你的代码更容易阅读。

我的想法是,“在函数中间”的返回不应该那么“主观”。 原因很简单,以下面的代码为例:

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;
}
也许第一个“返回”不是那么直观,但这是真正的节省。 有太多关于干净代码的“想法”,只是需要更多的实践来丢掉他们“主观的”坏想法

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

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

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

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

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

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

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

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

这里有几个很好的点,但如果方法非常长,也有多个返回点可能是不可读的。也就是说,如果你打算使用多个返回点,只要确保你的方法是简短的,否则多个返回点的可读性奖励可能会丢失。

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

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

如果你实际上返回一个returnValue,嵌套通常是一个更好的方式,因为你只在一个地方返回你的returnValue(在末尾),它可能会使你的代码在很多情况下更易于维护。

关于代码看起来如何有很多好的理由。但是结果呢?

让我们来看看一些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. 如果是true则跳转到最后一条指令。(注意:最后一个指令是return)。
  3. 在第二种情况下,计算结果后也会发生同样的情况。比较和:如果是false则到达Console.WriteLine,如果是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,跳转到结尾。

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

就我个人而言,我倾向于只有一个退出点。如果您保持方法的简短和切中要害,这很容易实现,并且它为下一个处理您的代码的人提供了可预测的模式。

如。

 bool PerformDefaultOperation()
{
bool succeeded = false;


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


return succeeded;
}

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

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

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

通过用保护子句替换嵌套,我甚至发现了自己的一个误解,即一部分清理代码应该在函数的早期出现,而不是在函数的末尾。对于嵌套分支,我从来没有看到过,你甚至可以说它们导致了我的误解。

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

这不仅仅是审美问题,但它也减少了方法内部的最大嵌套水平。这通常被认为是一个加分项,因为它使方法更容易理解(实际上,许多 静态 分析 工具提供了一种度量方法,作为代码质量的指标之一)。

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

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

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

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

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

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

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

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

一种观点是其他人已经提到的观点:第二种方法减少了嵌套级别,从而提高了代码的清晰度。这在命令式风格中是很自然的:当你没有什么事情要做的时候,你不妨早点回来。

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

表演分为两部分。在软件处于生产状态时,您需要性能,但在开发和调试时也需要性能。开发人员最不想做的事情就是“等待”一些微不足道的事情。最后,在启用优化的情况下编译该代码将生成类似的代码。所以知道这些在两种情况下都有好处的小技巧是很好的。

问题中的情况很清楚,ReSharper是正确的。不是嵌套if语句,并在代码中创建新的作用域,而是在方法的开始设置一个明确的规则。它增加了可读性,更容易维护,并减少了人们必须筛选的规则数量。

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

因此,在某些情况下,看起来是一个正确的ifif可能不是。

我想补充的是,这些倒置的if是有名称的-保护条款。我一有机会就用它。

我讨厌阅读一开始只有两屏代码的代码。只要求if的倒数,然后返回。这样就没人会浪费时间刷屏幕了。

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

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

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

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

更多关于分支预测在这里

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

条件false ->远跳到false_condition_label

< p > true_condition_label: instruction1 ... instruction_n < / p > < p > false_condition_label: instruction1 ... instruction_n < / p >

结束块

如果condition为真,则没有跳转,也没有L1缓存的滚出,但是跳转到false_condition_label可能很远,处理器必须滚出自己的缓存。同步缓存的开销很大。r#尝试将远跳转替换为短跳转,在这种情况下有更大的概率,所有指令都已经在缓存中。