偶尔我喜欢花一些时间查看.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
}
检查this
对null
的原因是什么?我必须假设有目的,否则这可能会被抓住并被删除。
答案 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)
。由此我们可以看到三件事:
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在调用方法时执行空检查。调用指令不允许输入方法,this
为null
。
声音吓人?确实有点儿。然而,大多数编译器确保不会发生这种情况。只有当null
不可能时才会输出.call指令(我很确定C#总是使用callvirt)。
但是对于所有语言都不是这样,并且由于我不确切知道BCL团队选择在此实例中进一步强化System.String
类。
另一种可以弹出的情况是反向pinvoke调用。
答案 2 :(得分:9)
简短的回答是像C#这样的语言迫使你在调用方法之前创建这个类的实例,但框架本身却没有。在CIL中有两种不同的方式来调用函数:call
和callvirt
....一般来说,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()方法,而不仅仅是其他字符串。