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

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


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

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

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

评论

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

大多数语言允许您选择所需的行为。在C#中,运算符||短路,操作员| (单管)不同(&&和&行为相同)。由于短路运算符的使用频率更高,因此我建议在注释中标记非短路运算符的用法,以解释为什么需要它们的行为(大多数情况下都需要进行方法调用之类的事情)。 />
@linac现在在&或|的右侧添加一些内容确保发生副作用是我不愿意采取的做法:将该方法调用放在自己的行上不​​会花费您任何费用。

@linac:在C和C ++中,&&短路而&&短路,但是它们是完全不同的运算符。 &&是一个逻辑“和”; &是按位“与”。 x && y可能为true,而x&y为false。

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

#1 楼

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

您的代码示例非常清楚,实际上,这通常是编写它的最佳方法。替代方法(例如嵌套的if语句)将变得更杂乱,更复杂,因此更难以理解。 br />
以下不是一个好主意:

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


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

如果需要处理错误,可以使用通常是更好的方法

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

避免重复检查相同的条件

正如尼尔所指出的,对相同的条件进行多次重复检查不同条件下的变量很可能是设计缺陷的证据。如果smartphonenull时,如果代码中散布了十条不同的语句不应该运行,请考虑是否有比每次检查变量更好的方法来处理此问题。关于短路这是重复代码的一个更普遍的问题。但是,值得一提的是,因为看到具有很多重复语句的代码(例如您的示例语句)是很常见的。

评论


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

–尼尔
15年5月20日在9:20



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

–user82096
15年5月20日在9:33

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

– BrianH
15年5月20日在20:41



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

– AaronLS
15年5月20日在20:57



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

–user82096
2015年5月21日在13:30

#2 楼

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

您的代码将是:



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


这种模式会出现很多问题。我们的假设语言介绍了&&。想想你觉得它有多酷!

&&是公认的惯用法,可以完成我上面的示例中的操作。使用它不是坏习惯,在上述情况下不使用它确实是一种坏习惯:具有这种语言的经验丰富的编码人员会想知道else在哪里或其他不按常规方式工作的原因。 />

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


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

聪明与聪明之间的区别很重要,因为好的坏习惯常常归结为您需要的聪明,但再聪明不过了。

考虑:

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


这会将您的代码扩展为检查range。如果range为null,则它会尝试通过调用GetRange()进行设置,尽管这可能会失败,因此range可能仍为null。如果此后range不为null,并且为Valid,则使用其MinSignal属性,否则使用默认值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风格语言重写它:

在我们对if(a)
{
  if(b)
  {
    // Do something
  }
}
的使用中隐藏了额外的if,但它仍然存在。因此,在这种情况下会发生分支预测,因此可能会发生分支错误预测。在这里,我要说&&仍然是开始时的最佳实践方法:更常见的是,在某些情况下唯一可行的方法(如果if(a & b)为false,则if(a && b)会出错)并且比其他情况更快。值得注意的是,尽管在某些情况下,b通常是对其有用的优化。

评论


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

–超级猫
15年5月20日在16:33

我没有发现if(TryThis || TryThat)有问题。

–琼·汉娜(Jon Hanna)
15年5月20日在16:36

太好了,先生。

–主教
2015年5月21日,3:26

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

–山姆·沃特金斯(Sam Watkins)
2015年5月21日在8:32



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

– gnasher729
15年5月22日在19:32

#3 楼

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

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

是惯用的。 open成功返回非零值(因此die之后的or部分永远不会发生),但失败时未定义,然后调用die确实发生(这是Perl引发异常的方式)。在每个人都可以使用的语言中很好。

评论


Perl对语言设计的最大贡献是提供了许多不这样做的示例。

–杰克·艾德利(Jack Aidley)
2015年5月20日15:14

@JackAidley:我想念Perl的,除非,但有后缀条件句(如果定义了$ address,则为send_mail($ address))。

– RemcoGerlich
2015年5月20日17:57

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

–巨石
2015年5月21日在5:59

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

–user82096
15年5月21日在13:26

@RemcoGerlich Ruby继承了某些样式:send_mail(address)if address

– Bonh
15年5月21日在15:03



#4 楼

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

问题是短路评估不是原子的。您的线程可以检查smartphonenull,另一个线程可以进入并使其无效,然后您的线程立即尝试GetSignal()并炸毁。

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

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

#5 楼

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

因此,迫切需要对逻辑表达式进行短路评估。 Pascal(无保证的短路评估)中不存在这种情况,这是一个主要的痛苦。我首先在Ada和C语言中看到过它。评估,然后再告诉我们您的喜欢程度。这就像被告知呼吸会产生二氧化碳,并询问呼吸是否是不良习惯。尝试不使用它几分钟。

评论


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

–托马斯·帕德隆·麦卡锡
2015年5月21日在12:21

#6 楼

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



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


可以简化为是:

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


如果currentUser默认为某些“匿名用户”或“空用户” ',如果用户未登录,则使用后备实现。

并非总是有代码味道,但要考虑一些事情。

#7 楼

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

ie



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


很好,但是

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


很糟糕,肯定没有人会同意吗?!

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


推理:

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

原因1.历史

本质上,您所依赖的(最初的意图是)编译器优化为您提供功能。码。

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

Wikipeda列出了多种语言,并对它们进行了短路。 。还有一些其他问题,我不再赘述。

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

因此:您可以发现您的代码在编译以供发行时有效,但不适用于调试或与其他编译器等结合使用。

原因2。语言差异语言即使在指定布尔运算符应如何处理时也采用相同的短路方法。

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

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

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

原因4。您的null检查参数的具体示例一个函数

这是一个很好的例子。这样做是每个人都可以做的,但是考虑到它实际上是一种不好的做法。

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

但是,它由于多种原因而未能通过最佳实践! >从许多来源都可以很清楚地看出,最佳实践是在使用参数之前检查其有效性,并在参数无效时引发异常。您的代码段未显示周围的代码,但您可能应该执行以下操作:

 if (smartphone == null)
{
    //throw an exception
}
// perform functionality
 



您正在条件语句中调用函数。尽管函数的名称暗示它只是计算一个值,但是它可以隐藏副作用。例如,

 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;
 


和null合并

 var s = smartphoneConnectionStatus ?? "No Connection";
 


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

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

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

C#中的构造函数参数验证-最佳实践

如果他不希望为null,是否应该检查null?

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}编译器指令将其关闭。“

评论


评论不作进一步讨论;此对话已移至聊天。

–世界工程师
15年5月22日在1:03