C# 为什么要检查这个!=无效的

C# 为什么要检查这个!=无效的,c#,.net,clr,reflector,C#,.net,Clr,Reflector,有时,我喜欢花一些时间看一下.NET代码,看看在幕后是如何实现的。我在通过Reflector查看字符串.Equals方法时偶然发现了这个宝石 C# IL .method public hidebysig virtual instance bool Equals(object obj) cil managed { .custom instance void System.Runtime.ConstrainedExecution.ReliabilityContractAttribute::.

有时,我喜欢花一些时间看一下.NET代码,看看在幕后是如何实现的。我在通过Reflector查看
字符串.Equals
方法时偶然发现了这个宝石

C#

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 
}

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

我假设您正在查看.NET 3.5的实现?我认为.NET4的实现略有不同

然而,我有一种潜在的怀疑,这是因为即使是虚拟实例方法,也可以对空引用进行非虚拟调用。也就是说,在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#代码得到的:

。。。然后使用
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()
那么.NET2.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()
现在这更有趣了。。。显然,我们已经设法进入了
equalHelper
,这是我们通常不会想到的

够多的绳子了。。。让我们自己尝试实现引用相等,看看是否可以得到
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++/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)
。从中我们可以看到三件事:

  • 你需要小心超载
  • C#编译器向虚方法的声明器发出调用,而不是虚方法最具体的重写。IIRC,VB的工作方式正好相反
  • object.Equals(object)
    乐于比较空的“this”引用
如果在C#覆盖中添加一点控制台输出,您可以看到区别——除非更改IL以显式调用它,否则不会调用它,如下所示:

IL_0005:  call   instance bool Test::Equals(object)
那么,我们到了。null引用上实例方法的有趣和滥用


如果你已经做到了这一点,你可能还想看看我关于。。。在IL.

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

这将保存一个函数调用。除此之外,更好地理解
EqualsHelper
函数可能有助于了解为什么需要这种优化

编辑:


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

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

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

但并非所有语言都是如此,因为我不清楚BCL团队在本例中选择进一步强化
System.String
类的原因


另一种情况是在反向pinvoke调用中会弹出此对话框

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

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

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

    int[] list = {1,2,3};
    Console.WriteLine("a string".Equals(list));
IL_0005: call instance bool Test::Equals(object)
    int[] list = {1,2,3};
    Console.WriteLine("a string".Equals(list));