例如,当我在我的代码上运行ReSharper时:
if (some condition)
{
Some code...
}
ReSharper给了我上面的警告(反转“if”语句以减少嵌套),并建议以下更正:
if (!some condition) return;
Some code...
我想知道为什么这样更好。我一直认为在方法中间使用“return”是有问题的,有点像“goto”。
例如,当我在我的代码上运行ReSharper时:
if (some condition)
{
Some code...
}
ReSharper给了我上面的警告(反转“if”语句以减少嵌套),并建议以下更正:
if (!some condition) return;
Some code...
我想知道为什么这样更好。我一直认为在方法中间使用“return”是有问题的,有点像“goto”。
当前回答
保护子句或先决条件(正如您可能看到的那样)检查是否满足某个条件,然后中断程序的流程。它们适用于只对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
返回远没有去到那么脏。它允许您传递一个值来显示函数无法运行的其余代码。
你将看到在嵌套条件下应用这个的最佳示例:
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会改变你的生活或让你上床,但你可能会发现你的代码更容易阅读。
其他回答
这里已经有很多有见地的答案,但是,我仍然想要指向一个稍微不同的情况:而不是前置条件,它实际上应该放在函数的顶部,考虑一步一步的初始化,在那里你必须检查每一步是否成功,然后继续下一步。在这种情况下,您不能检查顶部的所有内容。
当我使用Steinberg的ASIOSDK编写ASIO主机应用程序时,我发现我的代码真的难以阅读,因为我遵循了嵌套范式。它有8层深,我看不出有什么设计缺陷,正如Andrew Bullock上面提到的那样。当然,我可以将一些内部代码打包到另一个函数中,然后在那里嵌套剩余的级别以使其更具可读性,但对我来说这似乎相当随机。
通过用保护子句替换嵌套,我甚至发现了自己的一个误解,即一部分清理代码应该在函数的早期出现,而不是在函数的末尾。对于嵌套分支,我从来没有看到过,你甚至可以说它们导致了我的误解。
所以这可能是另一种情况,反向if可以有助于更清晰的代码。
从理论上讲,反转if可以提高分支预测的命中率,从而提高性能。在实践中,我认为很难确切地知道分支预测将如何表现,特别是在编译之后,所以我不会在日常开发中这样做,除非我正在编写汇编代码。
更多关于分支预测的信息。
关于代码的外观有很多很好的理由。但是结果如何呢?
让我们来看看一些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代码执行以下操作:
如果第一个条件为假,则跳转到第二个条件所在的代码。 如果为真,则跳转到最后一条指令。(注意:最后一个指令是return)。 在第二种情况下,计算结果后也会发生同样的情况。比较和:到达控制台。如果为假则写eline,如果为真则写到结尾。 打印消息并返回。
所以看起来代码会跳到最后。如果我们对嵌套代码做一个正常的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个跳跃(编译器优化了一点):
第一次跳转:当Length为0到代码再次跳转(第三次跳转)的部分时。 第二:在第二种情况中间避免一条指令。 第三:如果第二个条件为假,跳到最后。
不管怎样,程序计数器总是会跳转。
There are several advantages to this sort of coding but for me the big win is, if you can return quick you can improve the speed of your application. IE I know that because of Precondition X that I can return quickly with an error. This gets rid of the error cases first and reduces the complexity of your code. In a lot of cases because the cpu pipeline can be now be cleaner it can stop pipeline crashes or switches. Secondly if you are in a loop, breaking or returning out quickly can save you a lots of cpu. Some programmers use loop invariants to do this sort of quick exit but in this you can broke your cpu pipeline and even create memory seek problem and mean the the cpu needs to load from outside cache. But basically I think you should do what you intended, that is end the loop or function not create a complex code path just to implement some abstract notion of correct code. If the only tool you have is a hammer then everything looks like a nail.
只在函数结束时返回的想法来自语言支持异常之前的时代。它使程序能够依赖于能够将清理代码放在方法的末尾,然后确保它会被调用,而其他程序员不会在方法中隐藏导致跳过清理代码的返回值。跳过清理代码可能导致内存或资源泄漏。
然而,在支持异常的语言中,它不提供这样的保证。在支持异常的语言中,任何语句或表达式的执行都可能导致导致方法结束的控制流。这意味着清理必须通过使用finally或使用关键字来完成。
不管怎样,我想说的是,我认为很多人引用了“只在方法末尾返回”的指导原则,却不理解为什么它是一件好事,而减少嵌套以提高可读性可能是一个更好的目标。