短路评估,这是不好的做法吗?


88

我已经知道了一段时间,但从未考虑过的一件事是,在大多数语言中,可以根据其顺序在if语句中为运算符赋予优先级。我经常用这种方法来防止空引用异常,例如:

if (smartphone != null && smartphone.GetSignal() > 50)
{
   // Do stuff
}

在这种情况下,代码将导致首先检查对象是否不为null,然后在知道该对象存在的情况下使用该对象。该语言很聪明,因为它知道如果第一条语句为假,那么即使评估第二条语句也没有意义,因此永远不会抛出空引用异常。对于andor运算符,其作用相同。

据我所知,这在其他情况下也很有用,例如检查索引是否在数组的边界内,并且可以用各种语言执行这种技术:Java,C#,C ++,Python和Matlab。

我的问题是:这种代码是否代表不良做法?这种不良做法是由某种隐藏的技术问题引起的(例如,这最终可能导致错误)还是对其他程序员而言导致可读性问题?会令人困惑吗?


69
技术术语是短路评估。这是一种众所周知的实现技术,在语言标准保证的情况下,依靠它是一件好事。(如果不是,那不是一种语言,IMO。)
Kilian Foth,2015年

3
大多数语言让您选择想要的行为。在C#中,操作员||短路,操作员|(单管)不短路(&&并且&行为相同)。由于短路运算符的使用频率更高,因此我建议在注释中标记非短路运算符的用法,以解释为什么需要它们的行为(大多数情况下都需要进行方法调用之类的事情)。
直线加速器2015年

14
@linac现在将某些东西放在右边,&|确保发生副作用是我肯定会不好的做法:将该方法调用放在自己的行上不​​会花费您任何费用。
乔恩·汉纳

14
@linac:在C和C ++中,&&是短路而&不是短路,但是它们是完全不同的运算符。&&是一个逻辑“与”;&是按位“和”。如果为假x && yx & y则可能为真。
基思·汤普森

3
您的特定示例由于其他原因可能是不好的做法:如果空,该怎么办?在此处为null是否合理?为什么它为空?如果该块之外的其他函数应该为空,则应该执行;整个事情应该什么都不做,还是程序应该停止?在每次访问中都写上“如果为空”(可能是由于您急于摆脱空引用异常)意味着您可以深入了解程序的其余部分,而无需注意电话对象初始化已被破坏起来。而最终,当你终于到达了一下代码,确实没有
Random832

Answers:


125

不,这不是一个坏习惯。依赖条件语句的短路是一种广泛接受的有用技术,只要您使用的语言可以保证这种行为即可(包括绝大多数现代语言)。

您的代码示例非常清楚,实际上,这通常是编写代码的最佳方法。备选方案(例如嵌套if语句)将更加混乱,更加复杂,因此更难以理解。

注意事项:

使用有关复杂性和可读性的常识

以下不是一个好主意:

if ((text_string != null && replace(text_string,"$")) || (text_string = get_new_string()) != null)

就像许多减少代码行数的“聪明”方法一样,过度依赖此技术可能会导致难以理解的,容易出错的代码。

如果您需要处理错误,通常有更好的方法

您的示例很好,只要它是智能手机为空的正常程序状态即可。但是,如果这是一个错误,则应合并更智能的错误处理。

避免重复检查相同情况

正如尼尔指出的那样,在不同条件下对相同变量进行多次重复检查很可能是设计缺陷的证据。如果你必须通过你的代码洒十个不同的声明,不应该被运行,如果smartphonenull,考虑是否有更好的方式来处理这个问题不是每次检查变量。

这实际上并不是专门针对短路进行的。这是重复代码的一个更普遍的问题。但是,值得一提的是,因为看到具有很多重复语句(例如您的示例语句)的代码非常普遍。


12
应该注意的是,如果在短路评估中检查实例是否为空的块检查重复,则可能应该重写为仅执行一次该检查。
尼尔

1
@Neil,好点;我更新了答案。

2
我唯一想补充的是,要注意另一种可选的处理方式,如果要在多个不同条件下检查某物,这确实是首选:检查if()中某物是否为null,然后返回/抛出-否则继续前进。这样就消除了嵌套,并且通常可以使代码更加简洁明了,例如“应该不为null,否则我就不在这里了”-尽可能谨慎地放置在代码中。任何时候编写的代码都可以在一个方法中的多个位置检查某个特定条件,这非常有用。
BrianH 2015年

1
@ dan1111我将您给出的示例概括为“条件不应该包含副作用(例如上面的变量赋值)。短路评估不会改变这一点,也不应该用作副作用的简写形式-可以很容易地放置在if主体中的效果,以更好地表示if / then关系。”
AaronLS 2015年

@AaronLS,我强烈反对“条件不应该包含副作用”的说法。条件不会造成不必要的混乱,但是只要代码清楚,我就可以在这种情况下产生副作用(甚至超过一个副作用)。

26

假设您使用的是不带C语言的语言,&&并且需要执行与问题中相同的代码。

您的代码为:

if(smartphone != null)
{
  if(smartphone.GetSignal() > 50)
  {
    // Do stuff
  }
}

这种模式会出现很多。

现在想象一下我们假设语言的2.0版引入了&&。想想你觉得那有多酷!

&&是我上面的示例所做的公认的惯用方法。使用它不是坏习惯,实际上,在上述情况下不使用它是坏习惯:有这样一种语言的经验丰富的编码人员会想知道else过去或其他原因不按常规方式工作。

该语言很聪明,因为它知道如果第一条语句为假,那么即使评估第二条语句也没有意义,因此永远不会抛出空引用异常。

不,很聪明,因为您知道如果第一条语句为假,那么即使评估第二条语句也没有意义。语言像一盒石头一样愚蠢,并且按照您的指示去做。(约翰·麦卡锡(John McCarthy)更加聪明,他意识到短路评估对于语言来说将是一件有用的事情)。

聪明与语言聪明之间的区别很重要,因为好的习惯和坏习惯常常归结为您在必要时会变得聪明,但不再聪明。

考虑:

if(smartphone != null && smartphone.GetSignal() > ((range = (range != null ? range : GetRange()) != null && range.Valid ? range.MinSignal : 50)

这会将您的代码扩展为检查range。如果the range为null,则尝试通过调用将其设置,GetRange()尽管可能会失败,所以range可能仍为null。如果range在此之后不为null,则使用ValidMinSignal属性,否则使用默认值50

这也取决于&&,但是将其放在一行中可能太聪明了(我不能100%地确定我做对了,并且我不会再次检查,因为事实证明了我的意思)。

虽然这不是&&这里的问题,但是使用它在一个表达式中添加很多内容的能力(一件好事)增加了我们不必要地编写难以理解的表达式的能力(一件坏事)。

也:

if(smartphone != null && (smartphone.GetSignal() == 2 || smartphone.GetSignal() == 5 || smartphone.GetSignal() == 8 || smartPhone.GetSignal() == 34))
{
  // do something
}

在这里,我将结合使用&&和检查某些值。对于电话信号,这是不现实的,但在其他情况下,确实会出现这种情况。在这里,这是我不够聪明的一个例子。如果执行以下操作:

if(smartphone != null)
{
  switch (smartphone.GetSignal())
  {
    case 2: case 5: case 8: case 34:
      // Do something
      break;
  }
}

我不仅在可读性方面而且在性能方面都有所提高(对的多个调用GetSignal()可能没有被优化)。

这里的问题还不&&只是拿起那把特定的锤子,而把其他东西都看成是钉子。不使用它会让我做得比使用它更好。

最后一个偏离最佳实践的情况是:

if(a && b)
{
  //do something
}

相比:

if(a & b)
{
  //do something
}

关于为什么我们会偏爱后者的经典论点是,在评估b我们想要是否a为真时有一些副作用。我不同意这一点:如果这种副作用是如此重要,请在单独的代码行中实现它。

但是,就效率而言,两者中的任何一个都可能更好。b前者显然将执行更少的代码(根本不会在一个代码路径中进行评估),这可以为我们节省评估所需的时间b

尽管第一个也有一个分支。考虑我们是否使用假设的C风格语言将其重写为no &&

if(a)
{
  if(b)
  {
    // Do something
  }
}

这笔额外费用if在我们对的使用中隐藏了&&,但仍然存在。因此,在这种情况下,会发生分支预测,因此可能会发生分支错误预测。

因此,if(a & b)在某些情况下代码可以更有效。

在这里,我要说if(a && b)的仍然是开始时的最佳实践方法:更常见的是,在某些情况下唯一可行的方法(b如果a为假则将出错),并且通常更快。值得注意的是,if(a & b)在某些情况下,这通常是有用的优化。


1
如果有成功的情况下try会返回的方法,true您怎么看if (TryThis || TryThat) { success } else { failure }?这是一个不太常用的习惯用法,但是我不知道如何在没有代码重复或添加标志的情况下完成同一件事。尽管从分析的角度来看,标志所需的内存是非问题的,但添加标志可以有效地将代码分成两个并行的Universe-一个包含在标志为时可访问的语句,true而另一个在它为时可访问的语句false。从DRY的角度看有时是好的,但有些棘手。
超级猫

5
我认为没有错if(TryThis || TryThat)
乔恩·汉纳

3
太好了,先生。
主教

FWIW,贝尔实验室的Kernighan,Pluuger和Pike等优秀的程序员为了清晰起见,建议使用浅控制结构(if {} else if {} else if {} else {}而不是嵌套控制结构)if { if {} else {} } else {},即使这意味着多次评估同一表达式也是如此。莱纳斯·托瓦尔兹(Linus Torvalds)说了类似的话:“如果您需要三个以上的压痕级别,那么无论如何您都会被搞砸了”。让我们以此作为对短路操作员的投票。
山姆·沃特金斯2015年

如果(a && b)陈述;很容易更换。如果(a && b && c && d)陈述1;其他声明2; 是一个真正的痛苦。
gnasher729

10

您可以更进一步,在某些语言中,这是惯用的处理方式:您也可以在条件语句之外使用短路求值,它们本身就成为条件语句的一种形式。例如在Perl中,对于函数来说,在失败时返回虚假的东西(在成功时返回虚假的东西)是很常见的,例如

open($handle, "<", "filename.txt") or die "Couldn't open file!";

是惯用的。open在成功时返回非零值(这样,die以后or发生的那部分永远不会发生),但在失败时返回未定义的值,然后调用die完成(这是Perl引发异常的方式)。

在每个人都可以使用的语言中,这很好。


17
Perl对语言设计的最大贡献是提供了许多这样做的示例。
杰克·艾德利

@JackAidley:我想念Perl的unlesssend_mail($address) if defined $address但后缀条件()。
RemcoGerlich 2015年

6
@JackAidley您能提供一个指示“或死”为什么不好的指针吗?这仅仅是处理异常的幼稚方式吗?
巨石2015年

在Perl中可以做很多可怕的事情,但是我看不到这个例子的问题。它简单,简短和清晰-正是您希望从脚本语言中进行错误处理所需要的。

1
@RemcoGerlich Ruby继承了某些样式:send_mail(address) if address
bonh,2015年

6

虽然我通常同意dan1111的回答,但是有一个特别重要的案例没有涵盖:smartphone并发使用的案例。在这种情况下,这种模式是众所周知的难以发现错误的来源。

问题在于短路评估不是原子的。你的线程可以检查smartphone就是null,另一个线程可以进来,废止它,然后你的线程立即尝试GetSignal()和炸毁。

但是,当然,只有当星星对齐并且线程的时机恰到好处时,它才会这样做。

因此,尽管这种代码非常普遍且有用,但是了解此警告很重要,这样您才能准确地防止此讨厌的错误。


3

在很多情况下,我想先检查一个条件,而如果第一个条件成功,则只想检查第二个条件。有时纯粹是出于效率考虑(因为如果第一个条件已经失败,则没有必要检查第二个条件),有时是因为否则我的程序会崩溃(首先检查NULL),有时是因为否则结果会很糟糕。(如果(我要打印文档)和(打印失败),然后显示错误消息-您不想打印文档,如果不想打印文档,请检查打印是否失败。第一名!

因此,迫切需要对逻辑表达式进行短路评估。Pascal(无保证的短路评估)中不存在这种情况,这是一个主要的痛苦。我第一次在Ada和C中看到它。

如果您确实认为这是“不好的做法”,那么请采用任何稍为复杂的代码,将其重写,以使其在没有短路评估的情况下也能正常工作,然后返回并告诉我们您如何喜欢它。这就像被告知呼吸会产生二氧化碳,并询问呼吸是否有害。尝试不使用它几分钟。


“重写它,使其在没有短路评估的情况下也能正常工作,然后回来告诉我们您的喜欢程度。” -是的,这带回了Pascal的回忆。不,我不喜欢它。
Thomas Padron-McCarthy

2

在其他答案中未提及的一个考虑因素:有时进行这些检查可能暗示可能会对Null Object设计模式进行重构。例如:

if (currentUser && currentUser.isAdministrator()) 
  doSomething();

可以简化为:

if (currentUser.isAdministrator())
  doSomething ();

如果currentUser用户未登录,则默认将if设置为具有后备实现的某些“匿名用户”或“空用户”。

并非总是有代码气味,而是需要考虑的事情。


-4

我会不受欢迎,说是,这是不好的做法。如果代码的功能依赖于它来实现条件逻辑。

 if(quickLogicA && slowLogicB) {doSomething();}

很好,但是

if(logicA && doSomething()) {andAlsoDoSomethingElse();}

是不好的,肯定没有人会同意吗?

if(doSomething() || somethingElseIfItFailed() && anotherThingIfEitherWorked() & andAlwaysThisThing())
{/* do nothing */}

推理:

如果双方都经过评估而不是简单地不执行逻辑,则您的代码将出错。有人认为这不是问题,因为“ and”运算符在c#中定义,所以含义很清楚。但是,我认为这是不正确的。

原因1.历史

本质上,您要依靠(最初的意图是)编译器优化来在代码中提供功能。

尽管在c#中,语言规范保证布尔值“ and”运算符会短路。并非所有的语言和编译器都如此。

Wikipeda列出了多种语言,它们对短路的影响http://en.wikipedia.org/wiki/Short-circuit_evaluation,因为您可以采用多种方法。还有一些其他问题,我不在这里讨论。

特别是,FORTRAN,Pascal和Delphi具有可切换此行为的编译器标志。

因此:您可能会发现您的代码在为发行而编译时有效,但对于调试或与其他编译器等而言则无效。

原因2.语言差异

从Wikipedia可以看出,并不是所有的语言都采用相同的短路方法,即使它们指定了布尔运算符应如何处理也是如此。

特别是VB.Net的“和”运算符不会短路,并且TSQL具有一些特殊性。

鉴于现代的“解决方案”可能包含多种语言,例如javascript,regex,xpath等,因此在使代码功能清晰时应考虑到这一点。

原因3.语言内的差异

例如,VB的内联if行为与其if不同。同样,在现代应用程序中,您的代码可能会在后面的代码和内联Web窗体之间移动,例如,您必须意识到含义的差异。

原因4。您的空检查函数参数的特定示例

这是一个非常好的例子。因为这是每个人都在做的事情,但是当您考虑时实际上是一种不好的做法。

看到这种检查非常常见,因为它可以大大减少代码行。我怀疑有人会在代码审查中提出它。(显然,从评论和评分中,您可以看到它很受欢迎!)

但是,它由于多种原因而无法通过最佳实践!

  1. 非常众多来源清楚,最好的做法是检查你的参数的有效性在使用之前,如果它们是无效抛出一个异常。您的代码段不显示周围的代码,但是您可能应该执行以下操作:

    if (smartphone == null)
    {
        //throw an exception
    }
    // perform functionality
    
  2. 您呼叫从一个函数的条件语句。尽管函数的名称暗示它只是计算一个值,但是它可以隐藏副作用。例如

    Smartphone.GetSignal() { this.signal++; return this.signal; }
    
    else if (smartphone.GetSignal() < 1)
    {
        // Do stuff 
    }
    else if (smartphone.GetSignal() < 2)
    {
        // Do stuff 
    }
    

    最佳做法将建议:

    var s = smartphone.GetSignal();
    if(s < 50)
    {
        //do something
    }
    

如果将这些最佳实践应用于您的代码,您会发现通过使用隐藏的条件来获得较短代码的机会消失了。最佳实践是做一些事情,以处理智能手机为空的情况。

我认为您会发现其他常见用法也是如此。您必须记住我们也有:

内联条件

var s = smartphone!=null ? smartphone.GetSignal() : 0;

和空合并

var s = smartphoneConnectionStatus ?? "No Connection";

提供类似的短代码长度功能,更加清晰

许多链接可以支持我的观点:

https://stackoverflow.com/questions/1158614/what-is-the-best-practice-in-case-one-argument-is-null

C#中的构造函数参数验证-最佳做法

如果他不期望为空,是否应该检查为空?

https://msdn.microsoft.com/zh-CN/library/seyhszts%28v=vs.110%29.aspx

https://stackoverflow.com/questions/1445867/why-would-a-language-not-use-short-circuit-evaluation

http://orcharddojo.net/orchard-resources/Library/DevelopmentGuidelines/BestPractices/CSharp

影响短路的编译器标志示例:

Fortran:“ sce | nosce默认情况下,编译器使用XL Fortran规则在选定的逻辑表达式中执行短路评估。指定sce允许编译器使用非XL Fortran规则。如果当前规则允许,编译器将执行短路评估。默认值为nosce。”

Pascal:“ B-一个控制短路评估的标志指令。有关短路评估的更多信息,请参见二元运算符的操作数评估顺序。有关命令行编译器的更多信息,请参见-sc编译器选项(记录在用户手册中。”

Delphi:“ Delphi默认情况下支持短路评估。可以使用{$ BOOLEVAL OFF}编译器指令将其关闭。”


评论不作进一步讨论;此对话已转移至聊天
世界工程师
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.