为什么要检查这个!= null?

偶尔,我想花一些时间看.NET代码,看看事情是如何在幕后实现的。 我偶然发现了这个gem,同时通过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 } 

检查null什么? 我必须假设有目的,否则这可能现在已经被捕获并被删除。

我假设你在看.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 ,这是我们通常没有想到的。

足够的string…让我们尝试自己实现引用的平等,看看我们是否可以得到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

请注意,虽然另一个答案引用了这个C ++的问题 ,但我们在这里更加狡猾,因为我们正在非虚拟地调用虚拟方法。 通常甚至C ++ / CLI编译器都会使用callvirt来实现虚拟方法。 换句话说,我认为在这个特例中,唯一的办法是用手写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这个引用

如果你添加了一些控制台输出到C#覆盖,你可以看到不同 – 它不会被调用,除非你改变IL来明确地调用它,如下所示:

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

所以,我们在那里。 有趣和滥用null引用上的实例方法。

如果你已经做到了这一点,你也可以看看我的博客文章, 如何值types可以声明无参数构造函数 …在IL。

原因是这确实有可能是null 。 有2个IL操作码可以用来调用一个函数:call和callvirt。 callvirt函数导致CLR在调用方法时执行空检查。 调用指令不会,因此允许input方法为null

听起来可怕吗? 的确有一点。 但是大多数编译器确保这一切都不会发生。 .call指令只在null不可能的情况下输出(我非常确定C#总是使用callvirt)。

对于所有语言来说,这并不是真的,因为我并不完全知道BCL团队select在此实例中进一步强化System.String类。

另一个可以popup的情况是反向的调用。

简单的答案是像C#这样的语言强制你在调用方法之前创build这个类的一个实例,但是框架本身不会。 在CIL中有两种不同的方式来调用一个函数: callcallvirt ….一般来说,C#总是会发出callvirt ,这就要求this不为null。 但其他语言(C + + / CLI想到)可以发出call ,这并没有这样的期望。

(好吧,如果你计算calli,newobj等,那更像是五个,但让我们保持简单)

源代码有这样的评论:

这是必要的,以防止反向pinvokes和其他呼叫者不使用callvirt指令

让我们看看… this是你比较的第一个string。 obj是第二个对象。 所以它看起来像是一种优化。 它首先将obj转换为stringtypes。 如果失败,则strB为空。 如果strB为空,那么它们肯定是不相等的, EqualsHelper函数可以被跳过。

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

编辑:

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

如果参数(obj)不会转换为string,则strB将为空,结果应该为false。 例:

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

false

请记住,string.Equals()方法是针对任何参数types调用的,不仅适用于其他string。