代码之家  ›  专栏  ›  技术社区  ›  Brian Gideon

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

  •  71
  • Brian Gideon  · 技术社区  · 15 年前

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

    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 ?我必须假设这是有目的的,否则到目前为止,这可能已经被捕获并移除了。

    6 回复  |  直到 11 年前
        1
  •  85
  •   Community CDub    8 年前

    我假设您正在研究.NET 3.5实现?我认为.NET 4实现略有不同。

    但是,我有一种潜移默化的怀疑,这是因为即使虚拟实例方法也可以非虚拟地调用 在空引用上 . 在伊利诺伊州是可能的。我去看看我能不能生产一些可以称之为 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 我们通常不会想到。

    足够的字符串…我们试着自己实现引用相等,看看是否可以 空。等于(空) 回归真实:

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

    与之前相同的步骤-拆解、更换 卡尔维尔特 呼叫 重新组装并观看打印 true

    请注意,尽管另一个答案引用了 this C++ question 我们在这里更加狡猾…因为我们打电话给 事实上的 方法不是虚拟的。正常情况下,即使c++/CLI编译器也会使用 卡尔维尔特 对于虚拟方法。换言之,我认为在这种情况下 this 为空就是手工写IL。


    编辑:我刚刚注意到一些事情……我实际上没有调用正确的方法 任何一个 我们的小样本程序。第一种情况是这样的:

    il_:调用实例bool[mscorlib]system.string::等于(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_override添加一点控制台输出,您可以看到不同之处-除非您更改IL以显式调用它,否则不会调用它,如下所示:

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

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

    如果你做到了这一点,你可能还想看看我关于 how value types can declare parameterless constructors …在IL。

        2
  •  17
  •   JaredPar    15 年前

    原因在于 this 成为 null . 有2个IL OP代码可用于调用函数:call和callvirt。callvirt函数使clr在调用方法时执行空检查。调用指令不允许,因此允许使用 存在 无效的 .

    听起来可怕吗?确实有一点。然而,大多数编译器都确保不会发生这种情况。.call指令只有在 无效的 这是不可能的(我很确定C总是使用callvirt)。

    但并非所有语言都是如此,出于某些原因,我不清楚BCL团队选择进一步强化 System.String 类。

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

        3
  •  9
  •   Warren Rumak    15 年前

    简而言之,像C这样的语言在调用方法之前强制您创建这个类的一个实例,但是框架本身并没有。CIL有两种不同的方法来调用函数: call callvirt …一般来说,C总是会发出 卡尔维尔特 ,这需要 this 不能为空。但是其他语言(C++/CLI)可以发出 呼叫 没有这种期望。

    (好吧,如果你数一数calli,newobj等的话,差不多是五个,不过我们还是简单点吧)

        4
  •  4
  •   Evgeniy Berezovsky    11 年前

    这个 source code 有此评论:

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

        5
  •  1
  •   Steve Wortham    15 年前

    让我们看看… this 是要比较的第一个字符串。 obj 是第二个对象。所以看起来这是一种优化。这是第一个演员 OBJ 字符串类型。如果失败了,那么 strB 是空的。如果 斯特布 是空的 不是,那么他们肯定不平等 EqualsHelper 可以跳过函数。

    这将保存一个函数调用。除此之外,也许更好地理解 均衡器 函数可以解释为什么需要这种优化。

    编辑:

    啊,所以equalshelper函数接受 (string, string) 作为参数。如果 斯特布 是空的,这就意味着它要么是一个空对象,要么不能成功地转换成字符串。 如果原因是 斯特布 如果为空,则表示对象是另一种类型,无法转换为字符串,那么您将不希望使用本质上两个空值调用EqualShelper(这将返回true)。 等号函数 应该 在这种情况下返回false。因此,如果语句不仅仅是一个优化,它实际上也确保了适当的功能。

        6
  •  0
  •   John Alexiou    15 年前

    如果参数(obj)未转换为字符串,则strb将为空,结果应为假。例子:

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

    false .

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