为什么检查这个!= null?

时间:2010-06-29 18:11:24

标签: c# .net clr reflector

偶尔我喜欢花一些时间查看.NET代码,看看幕后如何实现。我在通过Reflector查看String.Equals方法时偶然发现了这个宝石。

C#

[ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
public override bool Equals(object obj)
{
    string strB = obj as string;
    if ((strB == null) && (this != null))
    {
        return false;
    }
    return EqualsHelper(this, strB);
}

IL

.method public hidebysig virtual instance bool Equals(object obj) cil managed
{
    .custom instance void System.Runtime.ConstrainedExecution.ReliabilityContractAttribute::.ctor(valuetype System.Runtime.ConstrainedExecution.Consistency, valuetype System.Runtime.ConstrainedExecution.Cer) = { int32(3) int32(1) }
    .maxstack 2
    .locals init (
        [0] string str)
    L_0000: ldarg.1 
    L_0001: isinst string
    L_0006: stloc.0 
    L_0007: ldloc.0 
    L_0008: brtrue.s L_000f
    L_000a: ldarg.0 
    L_000b: brfalse.s L_000f
    L_000d: ldc.i4.0 
    L_000e: ret 
    L_000f: ldarg.0 
    L_0010: ldloc.0 
    L_0011: call bool System.String::EqualsHelper(string, string)
    L_0016: ret 
}

检查thisnull的原因是什么?我必须假设有目的,否则这可能会被抓住并被删除。

6 个答案:

答案 0 :(得分:85)

我假设你在看.NET 3.5实现?我相信.NET 4的实现略有不同。

然而,我有一种潜在的怀疑,这是因为甚至可以在空引用上非虚拟地调用虚拟实例方法。可能在IL中,即。我会看看是否可以产生一些会调用null.Equals(null)的IL。

编辑:好的,这是一些有趣的代码:

.method private hidebysig static void  Main() cil managed
{
  .entrypoint
  // Code size       17 (0x11)
  .maxstack  2
  .locals init (string V_0)
  IL_0000:  nop
  IL_0001:  ldnull
  IL_0002:  stloc.0
  IL_0003:  ldloc.0
  IL_0004:  ldnull
  IL_0005:  call instance bool [mscorlib]System.String::Equals(string)
  IL_000a:  call void [mscorlib]System.Console::WriteLine(bool)
  IL_000f:  nop
  IL_0010:  ret
} // end of method Test::Main

我通过编译以下C#代码得到了这个:

using System;

class Test
{
    static void Main()
    {
        string x = null;
        Console.WriteLine(x.Equals(null));

    }
}

...然后用ildasm反汇编并进行编辑。请注意这一行:

IL_0005:  call instance bool [mscorlib]System.String::Equals(string)

最初,那是callvirt而不是call

那么,当我们重新组装时会发生什么?好吧,使用.NET 4.0,我们得到了这个:

Unhandled Exception: System.NullReferenceException: Object
reference not set to an instance of an object.
    at Test.Main()

嗯。用.NET 2.0怎么样?

Unhandled Exception: System.NullReferenceException: Object reference 
not set to an instance of an object.
   at System.String.EqualsHelper(String strA, String strB)
   at Test.Main()

现在更有趣了......我们已经明确设法进入EqualsHelper,这是我们通常不会想到的。

足够的字符串...让我们自己尝试实现引用相等,看看我们是否可以让null.Equals(null)返回true:

using System;

class Test
{
    static void Main()
    {
        Test x = null;
        Console.WriteLine(x.Equals(null));
    }

    public override int GetHashCode()
    {
        return base.GetHashCode();
    }

    public override bool Equals(object other)
    {
        return other == this;
    }
}

与之前相同的程序 - 反汇编,将callvirt更改为call,重新组合,并观看它打印true ...

请注意,虽然另一个答案引用this C++ question,但我们在这里更加狡猾......因为我们非虚拟地调用虚拟方法。通常,即使C ++ / CLI编译器也会使用callvirt作为虚方法。换句话说,我认为在这种特殊情况下,this为空的唯一方法是手工编写IL。


编辑:我刚注意到一些事情......我实际上并没有在我们的小样本程序的 中调用正确的方法。这是第一种情况下的电话:

IL_0005:  call instance bool [mscorlib]System.String::Equals(string)

这是第二个电话:

IL_0005:  call instance bool [mscorlib]System.Object::Equals(object)

在第一种情况下,我意味着呼叫System.String::Equals(object),而在第二种情况下,我意味着呼叫Test::Equals(object)。由此我们可以看到三件事:

  • 你需要小心超载。
  • C#编译器发出对虚方法的声明符的调用 - 而不是虚拟方法的最具体的覆盖。 IIRC,VB以相反的方式工作
  • object.Equals(object)很乐意比较null“this”参考

如果向C#覆盖添加一些控制台输出,您可以看到区别 - 除非您更改IL以明确调用它,否则不会调用它,如下所示:

IL_0005:  call   instance bool Test::Equals(object)

所以,我们有。在空引用上有趣和滥用实例方法。

如果你已经做到这一点,你可能还想查看我在IL中关于how value types can declare parameterless constructors ...的博文。

答案 1 :(得分:17)

原因是this确实可能是null。有2个IL操作码可用于调用函数:call和callvirt。 callvirt函数使CLR在调用方法时执行空检查。调用指令不允许输入方法,thisnull

声音吓人?确实有点儿。然而,大多数编译器确保不会发生这种情况。只有当null不可能时才会输出.call指令(我很确定C#总是使用callvirt)。

但是对于所有语言都不是这样,并且由于我不确切知道BCL团队选择在此实例中进一步强化System.String类。

另一种可以弹出的情况是反向pinvoke调用。

答案 2 :(得分:9)

简短的回答是像C#这样的语言迫使你在调用方法之前创建这个类的实例,但框架本身却没有。在CIL中有两种不同的方式来调用函数:callcallvirt ....一般来说,C#将始终发出callvirt,这需要this不能空值。但其他语言(C ++ / CLI可以想到)可能会发出call,但没有那种期望。

(ok,如果算上愈伤组织,newobj等等,它更像是五个,但让我们保持简单)

答案 3 :(得分:4)

source code有此评论:

  

这是防止反向pinvokes和其他来电者的必要条件   谁不使用callvirt指令

答案 4 :(得分:1)

让我们看看... this是您要比较的第一个字符串。 obj是第二个对象。所以看起来它是各种各样的优化。它首先将obj转换为字符串类型。如果失败,则strB为空。如果strB为空,而this不为,则它们肯定不相等,可以跳过EqualsHelper函数。

这将保存函数调用。除此之外,或许更好地理解EqualsHelper函数可能会说明为什么需要进行这种优化。

编辑:

啊,所以EqualsHelper函数接受(string, string)作为参数。如果strB为null,那么这实际上意味着它要么是一个开头的空对象,要么无法成功地转换为字符串。 如果strB为null的原因是该对象是一个无法转换为字符串的不同类型,那么您不希望调用EqualsHelper,基本上有两个空值(这将返回true)。在这种情况下,Equals函数应该返回false。因此,这个if语句不仅仅是一个优化,它实际上也确保了正确的功能。

答案 5 :(得分:0)

如果参数(obj)没有强制转换为字符串,则strB将为null,结果应为false。示例:

    int[] list = {1,2,3};
    Console.WriteLine("a string".Equals(list));

false

请记住,对于任何参数类型都会调用string.Equals()方法,而不仅仅是其他字符串。