为什么检查这个! = 无效?

有时我喜欢花一些时间看看。NET 代码,只是为了看看事情是如何在幕后实现的。我偶然发现了这个宝石,而看着 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的原因是什么?我必须假设有目的,否则这可能已经被捕捉和删除了。

8402 次浏览

我想你是在研究.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

那么,当我们重新组装它的时候会发生什么呢:

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;
}
}

同样的程序之前-拆卸,改变 callvirtcall,重新组装,并观看它打印 true..。

请注意,虽然另一个答案引用 这个 C + + 的问题,我们在这里更加狡猾... 因为我们正在非虚拟地调用一个 虚拟的方法。通常,即使是 C + +/CLI 编译器也会将 callvirt用于虚方法。换句话说,我认为在这种特殊情况下,this为 null 的唯一方法是手工编写 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)乐于比较空的“ this”引用

如果您向 C # 覆盖添加一点控制台输出,您可以看到不同之处——除非您更改 IL 以显式调用它,否则不会调用它,如下所示:

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

所以,就是这样,在 null 引用上实例方法的有趣和滥用。

如果你已经做到这一点,你可能也想看看我的博客文章关于 值类型 < em > 如何声明无参数构造函数... 在 IL。

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

这将保存一个函数调用。除此之外,对 EqualsHelper函数的更好理解也许可以解释为什么需要这种优化。

编辑:

所以 EqualsHelper 函数接受 (string, string)作为参数。如果 strB为 null,那么这实质上意味着它要么是一个 null 对象,要么不能成功地转换为字符串。在这种情况下,Equals 函数 应该返回 false。因此,if 语句不仅仅是一个优化,它实际上还确保了正确的功能。

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

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

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

另一种可能弹出的情况是在反向 pcall 调用中。

简而言之,像 C # 这样的语言强制您在调用方法之前创建这个类的实例,但是 Framework 本身不这样做。在 CIL 中有两种不同的方法来调用函数: callcallvirt..。一般来说,C # 总是发出 callvirt,这要求 this不为空。但是其他语言(我想到的是 C + +/CLI)可以发出 call,它没有这样的期望。

(好吧,如果你算上 calli,newobj 等等,它更像是5,但是让我们保持简单)

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

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

false

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

源代码有以下评论:

这是必要的,以防止反向发送和其他呼叫 不使用 Callvirt 指令的用户