为什么检查这个!= null?

Bri*_*eon 71 .net c# clr reflector

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

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);
}
Run Code Online (Sandbox Code Playgroud)

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 
}
Run Code Online (Sandbox Code Playgroud)

什么是检查的理由this反对null?我必须假设有目的,否则这可能会被抓住并被删除.

Jon*_*eet 85

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

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

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

.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
Run Code Online (Sandbox Code Playgroud)

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

using System;

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

    }
}
Run Code Online (Sandbox Code Playgroud)

...然后拆解ildasm和编辑.注意这一行:

IL_0005:  call instance bool [mscorlib]System.String::Equals(string)
Run Code Online (Sandbox Code Playgroud)

最初,那callvirt不是call.

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

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

嗯.用.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()
Run Code Online (Sandbox Code Playgroud)

现在这更有意思......我们已经明确地设法进入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;
    }
}
Run Code Online (Sandbox Code Playgroud)

相同的步骤,之前-拆卸,更改callvirtcall,重新组装,然后看着它打印true...

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


编辑:我刚刚发现了一些......我本来就不是调用正确的方法无论是我们的小示例程序.这是第一种情况下的电话:

IL_0005:  call instance bool [mscorlib]System.String::Equals(string)
Run Code Online (Sandbox Code Playgroud)

这是第二个电话:

IL_0005:  call instance bool [mscorlib]System.Object::Equals(object)
Run Code Online (Sandbox Code Playgroud)

在第一种情况下,我打算打电话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)
Run Code Online (Sandbox Code Playgroud)

那么,我们就是.在空引用上有趣和滥用实例方法.

如果你做了这一步,你可能也想看看我的博客文章约值类型如何可以声明参数构造函数 ...在IL.

  • Freakin'Jon Skeet.我知道你疯了,擅长一切,但这太荒谬了.我会把帽子给你,但我不戴.这是一个upvote而不是你的厚颜无耻. (12认同)
  • /我抓起一些爆米花等待节目. (8认同)

Jar*_*Par 17

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

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

但是对于所有语言都不是这样,并且由于我不确切知道BCL团队System.String在这种情况下选择进一步强化该类.

弹出的另一种情况是反向pinvoke调用.


War*_*mak 9

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

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