73

有时我喜欢花一些时间查看 .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);
}

伊利诺伊州

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

this检查的理由是什么null?我必须假设这是有目的的,否则这可能现在已经被抓住并删除了。

4

6 回答 6

85

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

但是,我有一个偷偷摸摸的怀疑,这是因为甚至可以在 null 引用上非虚拟地调用虚拟实例方法。在 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

我通过编译以下 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;
    }
}

与以前相同的程序 - 拆卸,更改callvirtcall重新组装,并观看它打印true......

请注意,虽然另一个答案引用了这个 C++ 问题,但我们在这里更加狡猾......因为我们以非虚拟方式调用虚拟方法。通常,即使是 C++/CLI 编译器也会使用callvirt虚拟方法。换句话说,我认为在这种特殊情况下,为thisnull 的唯一方法是手动编写 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)

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

如果您已经做到了这一点,您可能还想查看我的博客文章,了解值类型如何IL 中声明无参数构造函数

于 2010-06-29T18:20:31.693 回答
17

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

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

但是,并非所有语言都如此,并且由于我不完全了解 BCL 团队System.String在这种情况下选择进一步强化课程的原因。

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

于 2010-06-29T18:24:27.810 回答
9

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

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

于 2010-06-29T18:24:30.520 回答
4

源代码有这样的注释:

这是防止反向调用和其他不使用 callvirt 指令的调用者所必需的

于 2014-06-13T03:31:08.953 回答
1

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

这将节省函数调用。除此之外,也许更好地理解该EqualsHelper函数可能会阐明为什么需要这种优化。

编辑:

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

于 2010-06-29T18:22:59.913 回答
0

如果参数 (obj) 未转换为字符串,则 strB 将为 null,结果应为 false。例子:

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

写道false

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

于 2010-06-30T19:32:11.380 回答