为什么要检查这个!= 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中有两种不同的方式来调用一个函数: call
和callvirt
….一般来说,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。