反转“if”语句以减less嵌套
当我在代码上运行ReSharper时,例如:
if (some condition) { Some code... }
ReSharper给了我上面的警告(反转“如果”的声明,以减less嵌套),并提出以下更正:
if (!some condition) return; Some code...
我想明白为什么这样更好。 我一直认为在一个方法中使用“return”是有问题的,有点像“goto”。
方法中的回报不一定是坏的。 如果代码的意图更清晰,最好立即返回。 例如:
double getPayAmount() { double result; if (_isDead) result = deadAmount(); else { if (_isSeparated) result = separatedAmount(); else { if (_isRetired) result = retiredAmount(); else result = normalPayAmount(); }; } return result; };
在这种情况下,如果_isDead
为真,我们可以立即退出该方法。 而是用这种方式来构build它可能会更好:
double getPayAmount() { if (_isDead) return deadAmount(); if (_isSeparated) return separatedAmount(); if (_isRetired) return retiredAmount(); return normalPayAmount(); };
我从重构目录中select了这个代码。 这个特定的重构称为:用嵌套条件replace嵌套条件。
它不仅美观 ,而且还降低了方法内的最大嵌套水平 。 这通常被认为是一个加号,因为它使得方法更容易理解(事实上, 许多 静态 分析 工具提供了这种作为代码质量指标之一的度量)。
另一方面,这也使得你的方法有多个出口点,而另一些人认为是不可行的。
就我个人而言,我同意ReSharper和第一个小组(用一种有例外的语言,我觉得讨论“多个退出点”是愚蠢的;几乎任何东西都可以抛出,所以在所有方法中有许多潜在的退出点)。
关于性能 :在每种语言中,两个版本应该是等价的(如果不是在IL级别,那么肯定在抖动通过代码之后)。 理论上这取决于编译器,但是实际上今天任何广泛使用的编译器都能够处理比这更先进的代码优化案例。
这是一个宗教论点,但我同意ReSharper,你应该更喜欢less嵌套。 我相信这超过了从一个函数有多个返回path的负面影响。
less嵌套的关键原因是提高代码的可读性和可维护性 。 请记住,许多其他开发人员将来需要阅读您的代码,而使用较less缩进的代码通常更容易阅读。
先决条件是一个很好的例子,可以在函数启动的早期返回。 为什么其他function的可读性会受到先决条件检查的影响?
至于从方法中多次返回的否定信息 – debugging器现在非常强大,而且很容易找出特定函数返回的位置和时间。
在一个函数中有多个返回不会影响维护程序员的工作。
糟糕的代码可读性会。
正如其他人所说,不应该有一个performance打击,但还有其他的考虑。 除了那些有效的担忧之外,在某些情况下,这也可能会让你陷入陷阱。 假设你正在处理一个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
不是这样的if
,似乎是正确的。
只有在函数结束时返回的想法从语言支持exception之前的日子就回来了。 它使程序能够依靠能够将清理代码放在方法的末尾,然后确定它将被调用,而其他程序员不会隐藏导致清理代码被跳过的方法的返回。 跳过清理代码可能会导致内存或资源泄漏。
但是,在支持exception的语言中,它没有提供这样的保证。 在支持exception的语言中,任何语句或expression式的执行都会导致导致方法结束的控制stream。 这意味着清理必须通过使用finally或using关键字来完成。
无论如何,我想我认为很多人引用“方法末尾的唯一回报”,却不明白为什么这是一件好事,为了提高可读性而减less嵌套可能是一个更好的目标。
我想补充一点,那就是那些倒掉的名字 – 守卫条款。 我可以随时使用它。
我讨厌阅读代码,如果在开始,有两个代码屏幕,没有其他的。 只要倒过来,并返回。 这样,没有人会浪费时间滚动。
它不仅影响美观,而且还防止代码嵌套。
它实际上可以作为确保您的数据有效的先决条件。
这当然是主观的,但是我认为这强烈地改善了两点:
- 现在很明显,如果
condition
成立,你的function就没有什么可做的了。 - 它保持了嵌套级别。 嵌套比可以想象的更易于阅读。
C中有多个返回点是一个问题(C ++程度较低),因为它们迫使您在每个返回点之前复制清理代码。 随着垃圾收集, try
| finally
构build和using
块,真的没有理由为什么你应该害怕它们。
最终归结为你和你的同事们更容易阅读。
在性能方面,两种方法之间没有明显的差异。
但是编码不仅仅是性能。 清晰度和可维护性也非常重要。 而且,在这种不影响性能的情况下,这是唯一重要的事情。
对于哪一种方法更可取,有各种各样的思想stream派。
一种观点是另一种观点:第二种方法降低了嵌套层次,这提高了代码的清晰度。 这是一种必然的风格:当你什么也没有做,你不如早日回来。
另一种观点,从更实用的angular度来看,一种方法应该只有一个出口点。 一切function语言都是一种expression。 所以如果语句必须总是有一个else子句。 否则,ifexpression式不会总是有一个值。 所以在function风格上,第一种方法更自然。
守卫条款或前提条件(你可能会看到)检查是否符合某个条件,然后中断程序的stream程。 对于那些你真的只对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
远不如去肮脏。 它允许你传递一个值来显示你的代码的其余部分,该函数不能运行。
您将看到可以在嵌套条件下应用的最佳示例:
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();
你会发现很less有人认为第一个更清洁,但当然,这是完全主观的。 一些程序员喜欢通过缩进知道什么样的条件,而我更愿意保持方法stream线性。
我不会build议precons会改变你的生活或让你奠定,但你可能会发现你的代码更容易阅读。
这里有好几个好点,但是如果方法很长,多个返回点也是不可读的 。 这就是说,如果你要使用多个返回点,只要确保你的方法是短的,否则多个返回点的可读性奖金可能会丢失。
我个人更喜欢只有1个退出点。 如果你保持方法简洁明了,并且为下一个工作在你的代码上的人提供了一个可预测的模式,那么这很容易实现。
例如。
bool PerformDefaultOperation() { bool succeeded = false; DataStructure defaultParameters; if ((defaultParameters = this.GetApplicationDefaults()) != null) { succeeded = this.DoSomething(defaultParameters); } return succeeded; }
如果您只想在函数退出之前检查函数中某些局部variables的值,这也非常有用。 所有你需要做的就是在最后的回报上放置一个断点,并且你保证会触发它(除非抛出exception)。
性能分为两部分。 当软件在生产时你有性能,但是你也想在开发和debugging的时候有性能。 开发人员想要的最后一件事就是“等待”一件小事。 最后,编译这个与优化启用将导致类似的代码。 所以了解这两种情况下的小技巧是很好的。
在问题中的情况是清楚的,ReSharper是正确的。 不是嵌套if
语句,而是在代码中创build新的作用域,而是在方法开始时设置明确的规则。 它增加了可读性,维护起来会更容易,而且减less了人们筛选要查找的规则的数量。
关于代码的样子有很多很好的理由。 但结果呢?
让我们来看看一些C#代码和它的IL编译forms:
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!!!"); } }
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代码执行以下操作:
- 如果第一个条件是错误的,则跳转到第二个条件所在的代码。
- 如果这是真的跳转到最后一条指令。 (注意:最后的指令是返回)。
- 在第二种情况下,计算结果之后会发生同样的情况。 比较和:到达Console.WriteLine(如果为false)或者如果这是真的结束。
- 打印消息并返回。
所以看起来代码会跳到最后。 如果我们使用嵌套代码进行正常的操作,该怎么办?
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代码stream动得更好,并有3个跳转(编译器优化了这一点):1.第一次跳转:当Length为0时,代码跳转到第三次跳转结束。 第二:在第二个条件中避免一条指令。 第三:如果第二个条件是错误的,跳到最后。
无论如何,程序计数器总是会跳转。
这简直是有争议的。 程序员之间并没有就提前回报问题达成一致。 就我所知,这总是主观的。
有可能做出一个表演的论点,因为最好有条件写出来,因为他们往往是真实的; 也可以认为它更清楚。 另一方面,它确实创build了嵌套testing。
我不认为你会得出这个问题的结论性答案。
从理论上讲,如果提高分支预测命中率,反转if会导致更好的性能。 实际上,我很难确切地知道分支预测的行为,特别是在编译之后,所以我不会在日常开发中这样做,除非我正在编写汇编代码。
更多关于分支预测这里 。
这是一个意见的问题。
我通常的做法是避免单行ifs,并返回一个方法的中间。
在你的方法中你不需要像这样的行,但是在你的方法的顶部检查一堆假设还有一些东西需要说明,而且如果它们全都通过了,你只能做实际的工作。
在我看来,如果你只是返回void(或者你永远不会检查的一些无用的返回代码),提前返回就没有问题,并且可能会提高可读性,因为你避免了嵌套,并且同时明确表示你的函数已经完成。
如果你实际上返回了一个returnValue – 嵌套通常是一个更好的方法,因为你只是在一个地方返回你的returnValue(在end – duh),它可能会让你的代码在很多情况下更易于维护。
那里已经有了很多有见地的答案,但是我仍然会指出一个稍微不同的情况:不是先决条件,而是应该放在函数的顶部,想一步一步的初始化,必须检查每一步成功,然后继续下一步。 在这种情况下,你不能检查顶部的所有内容。
当我使用Steinberg的ASIOSDK编写ASIO主机应用程序时,我发现我的代码真的不可读,因为我遵循了嵌套范式。 它像八层深,我看不出有一个devise缺陷,如上面安德鲁·布洛克所说。 当然,我可以将一些内部代码打包到另一个函数中,然后在其中嵌套剩余的关键字以使其更具可读性,但这对我来说似乎相当随意。
通过用守卫子句取代嵌套,我甚至发现了一个关于清理代码的错误概念,这个错误应该在函数内部早些时候发生,而不是在最后。 嵌套分支,我永远不会看到,你甚至可以说他们导致了我的误解。
所以这可能是另一种情况,倒转ifs可以有助于更清晰的代码。
避免多个退出点可能会导致性能提升。 我不确定C#,但在C ++中,命名返回值优化(Copy Elision,ISO C ++ '03 12.8 / 15)取决于具有单个退出点。 这种优化避免了复制构造你的返回值(在你的具体例子中并不重要)。 这可能会导致在紧密循环中的性能显着提高,因为每次调用函数时都要保存一个构造函数和一个析构函数。
但是,对于保存附加的构造函数和析构函数调用的情况, if
块引入(如其他人所指出的),则99%的情况不值得丢失可读性。
我认为这取决于你喜欢什么,如上所述,没有一般的协议afaik。 为了减less烦恼,你可以减less这种警告“提示”
我的想法是,“在function中间”的回归不应该如此“主观”。 原因很简单,拿这个代码:
函数do_something(data){ 如果(!is_valid_data(data)) 返回false; do_something_that_take_an_hour(data); istance = new object_with_very_painful_constructor(data); 如果(istance无效){ 错误信息( ); 返回; } connect_to_database(); get_some_other_data(); 返回; }
也许第一个“回报”不是那么直观,但这真的是节省。 关于清洁法规有太多的“想法”,只是需要更多的实践来失去他们的“主观”坏主意。
这种编码有几个优点,但对我来说,最重要的是,如果你能快速返回,你可以提高你的应用程序的速度。 IE我知道,因为Precondition X,我可以很快返回一个错误。 这首先摆脱了错误的情况,并降低了代码的复杂性。 在很多情况下,因为CPUpipe道现在可以更清洁,它可以停止pipe道崩溃或开关。 其次,如果你在一个循环中,快速断开或返回可以为你节省大量的CPU。 一些程序员使用循环不变式来做这种快速的退出,但是在这里你可能会破坏你的CPUpipe道,甚至造成内存寻道问题,这意味着CPU需要从外部caching中加载。 但基本上我认为你应该做你想做的事情,那就是结束循环或者函数不是创build一个复杂的代码path来实现一些抽象的正确代码的概念。 如果你有唯一的工具是锤子,那么一切都像一个钉子。