在字面值的相等比较中,操作数的顺序是否重要?

6
我习惯这样写代码(只是一个例子):
Request.QueryString["xxxx"] != null

最近有人说

null != Request.QueryString["xxxx"]

提供更好的性能。

我很好奇它是否真的带来了任何差别,如果有,那么是如何做到的?

请注意~以上仅为示例。一般而言

无论

Constant [Operator] Actual Value (e.g. 1 == Convert.ToInt32(textbox1.text))

更好。
Actual Value [Operator] Constant (e.g. Convert.ToInt32(textbox1.text) == 1)

谢谢

4个回答

15

什么是Yoda编码风格

它是一种编码风格,用于具有许多隐式类型转换的语言(因为字面上的顺序类似《星球大战》中的角色Yoda,即OSV单词顺序)。有时甚至会被强制执行,并在项目指南中要求使用,以防止由于拼写错误导致的某些错误。不同的语言有不同的变体和扩展,但本回答将仅限于C和C#。

为什么要使用Yoda编码风格

例如,在C语言中,您可以编写:

int a = CalculateValue();
if (a = CalculateAnotherValue()) {
    /* Do something */
}
该代码将从CalculateValue()返回的值赋给a,然后用CalculateAnotherValue()的结果覆盖该值,如果不为零,则执行该代码。这可能不是预期的操作,if表达式中的赋值是一个错误。根据您使用NULL的示例,您可能有:
if (pBuffer = NULL)

再次强调,这可能不是您想要的(这是一个非常常见的错误),您将向指针赋值 NULL 并且条件将始终为 false。 如果您写:

if (NULL = pBuffer)

它不会编译(因为您无法对字面值赋值),并且您将获得编译时错误。

这种编译时检查并不是使用这种编码风格的唯一原因,看看这个C#(非常常见)代码:

if (text != null && text.Equals("something", StringComparison.InvariantCulture)
    DoSomething();

它可以被简化为:

if ("something".Equals(text, StringComparison.InvariantCulture))
    DoSomething();

为什么不行

C#中通常情况下这并不重要。这是从C/C++继承来的一种实践方式。因为在C#中表达式不会自动转换为bool,所以以下代码将无法编译:

if (Request.QueryString["PartnerID"] = null)

那么在C#中这种做法是无用的(除了@IlianPinzon在评论中指出的例外情况),它只是用来避免此类错误,并没有与性能有关。

关于为什么是部分中的最后一个示例,问题在于可读性,写"something".Equals(text)就像说“如果快乐是那个人”而不是“如果那个人快乐”。

性能

从这些函数开始:

static bool TestRight(object value)
{ return value == null; }

static bool TestLeft(object value)
{ return null == value; }

他们生成以下IL:

.maxstack 2
.locals init ([0] bool CS$1$0000)
L_0000: nop 
L_0001: ldnull 
L_0002: ldarg.0 
L_0003: ceq 
L_0005: stloc.0 
L_0006: br.s L_0008
L_0008: ldloc.0 
L_0009: ret 
唯一的区别在于L_0001和L_0002行,它们只是交换了位置,但操作数的顺序不会改变ceq的行为。即使您覆盖了Equals()方法,JIT编译器也将为两个表达式生成相同的汇编代码(因为比较总是由Equals()完成的,null是无类型的)。
如果比较涉及用户定义的相等比较器,情况可能会更加复杂,在这种情况下,没有规则,它将取决于有效的op_Equals实现。例如,这是一个==的实现:
public static bool operator==(MyType lhs, MyType rhs)
{
    if (Object.ReferenceEquals(lhs, rhs))
        return true;

    if (Object.ReferenceEquals(lhs, null))
        return false;

    return lhs.Equals(rhs);
}

如果第一个运算数是null,执行速度会稍微快一些(因为甚至不会调用MyType.Equals()),但这种性能提升非常小:你只能节省一个比较、几次跳转和一个虚函数调用。此外,你可以重写该函数以使其在相反的情况下更快(如果你真的关心这一点)。

下面是一个小测试,其中MyType.Equals(object)对于任何非null参数都返回true。测试将被循环执行Int32.MaxValue次:

操作            总时间(毫秒)
lhs == null               10521
null == lhs                2346

似乎“优化”的版本避免了不必要的Equals()调用,速度快了五倍,但请注意,循环计数非常高,并且Equals()的实际实现是空的,真正的实现将减少函数调用的相对开销(也可能会有其他微小优化)。对于系统类,你不能依赖这些细节,例如String.Equals()总是由操作员调用(无论顺序如何),但你不能假设一个代码路径更快,并且这个事实在框架的未来版本(或不同的CPU体系结构)中也不会改变。


我很好奇,听说过它,但我不熟悉C/C++。为什么常量通常放在左边? - Matten
@Adriano:有一种情况,在C#中你仍然可能会被它咬到。尝试用“bool”替换“int”。但是,是的,在C#中,Yoda条件已经过时了。 - Ilian
@IlianPinzon 你说得对,使用bool确实有意义,但是...我非常不喜欢它,因为可读性太差了(个人口味,我知道),所以我愿意付出代价 :) - Adriano Repetti
1
@Matten 我添加了一些例子。 - Adriano Repetti
@Matten - 惯例是始终将常量放在表达式的左侧,以便在编码时将 =(赋值)错误地编码为预期的 ==(相等比较)时,在编译时静态检测到错误。现在我看到这个问题已经被回答了。太晚了。 - Peter Wone
1
+1 对于用户重载比较运算符的评论。 - Hristo Iliev

4

不,一般而言这是不正确的。比较运算符需要评估它们两侧,因此将常量放在左侧并没有优势。但在将它们放在左侧(称为Yoda风格)可以降低编码错误的风险,在某些允许在条件语句中使用赋值运算符的语言中,当您无意中将比较运算符 == 错写成单个=时,常量在左侧会减少误操作的可能性。

// What you intended to write
if (a == 6) ...
// What you wrote instead
if (a = 6) ... // --> always true as the value of a is changed to 6
// What if Yoda style is used
if (6 = a) ... // --> compile time error

这不是唯一的原因,在与null比较时。在某些情况下,它可以防止过载的"=="运算符陷入无限循环,尤其是如果类来自第三方库并且你不能百分之百信任它们的情况下。 - Kryptos

2
这是一个风格问题。有些人喜欢写
if (N == var) ...

如果您错过了一个=(对于初学者来说很常见),就会出现问题。

if (N = var)

如果N是一个常量,编译器会报错。

在下列情况下:

if (var = N)

它可能会给你一个警告,也可能不会(取决于编译器和标志)。因此,有时很难找到问题所在。

这两个变体的性能是相同的。选择您的风格并遵循它。


0

常量[运算符]实际值被称为 Yoda条件,因为它就像“如果是蓝色-这是天空”或“如果那个高-这是一个人”。

在C/C++中,使用这种条件风格很流行,您可以在键入=而不是==时将值分配给变量。在C#中,使用Yoda条件没有意义。

我也怀疑它是否具有更好的性能。即使它具有一些性能优势,它也会非常小。比可读性缺点还要小。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接