【问题标题】:Why check this != null?为什么要检查这个!= null?
【发布时间】:2011-03-09 18:44:08
【问题描述】:

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

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

【问题讨论】:

  • 你也可以看看 EqualsHelper 吗?他们似乎想使用 EqualsHelper,但它可能无法按照他们想要的方式处理空值。
  • 这特别有趣,因为文档明确指出,如果实例为空,Equals 将抛出 NullReferenceException ......
  • 我猜这要么是疏忽,要么与EqualsHelper 的工作方式有关。我根本看不出需要if 语句,假设strBnullthis 不是时EqualsHelper 将返回false。但也许我不够聪明,无法理解:)
  • @womp - 在 4.0 中,第一行是 if(this == null) throw new NullReferenceException(),所以在这个意义上是正确的。
  • 这段代码是在 1999 年 12 月 13 日之前编写的,那一天 C# 团队致力于如何实现空值检查。 blogs.msdn.com/b/ericgu/archive/2008/07/02/…

标签: c# .net clr reflector


【解决方案1】:

我假设您正在查看 .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,但我们在这里更加狡猾...因为我们以非虚拟方式调用 virtual 方法。通常,即使是 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# 编译器发出对虚拟方法的 declarer 的调用,而不是虚拟方法的最具体的 override。 IIRC,VB 的工作方式相反
  • object.Equals(object) 很乐意比较一个空的“this”引用

如果您在 C# 覆盖中添加一些控制台输出,您可以看到差异 - 除非您更改 IL 以显式调用它,否则它不会被调用,如下所示:

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

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

如果您已经做到了这一点,您可能还想查看我关于 IL 中 how value types can declare parameterless constructors... 的博文。

【讨论】:

  • 这台机器上没有 .NET 4。看看该版本中的实现是什么样子会很有趣。有趣的假设。让我们知道您的发现。
  • 令人着迷。因此,铁杆 IL 程序员可能会使用我开发不正确的 API,而我可以防止它的唯一方法是在每个实例方法上检查 this != null?想想如果所说的库与安全相关的影响!
  • 该死的乔恩·斯基特。我知道你很疯狂,什么都擅长,但这太荒谬了。我会向你脱帽致敬,但我没有戴。取而代之的是,这是一个赞成票,你这个厚脸皮的无赖。
  • @Brian,我的猜测是抖动在 .NET4 中添加了对非空 arg0 的检查以进行调用,而 .NET2 仅对 callvirt 执行了此检查(这绝对是必要的,因为它需要获取虚拟方法表。)
  • @Dan:不,我相信非空值检查是在 .NET 4 代码中的 IL 中执行的,我们没有看到它的唯一原因是内联。
【解决方案2】:

原因是this确实有可能是null。有 2 个 IL 操作码可用于调用函数:call 和 callvirt。 callvirt 函数使 CLR 在调用该方法时执行空值检查。 call 指令没有,因此允许使用thisnull 输入方法。

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

但并非所有语言都如此,出于我不完全了解 BCL 团队在这种情况下选择进一步强化 System.String 类的原因。

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

【讨论】:

    【解决方案3】:

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

    (¹好吧,如果算上 calli、newobj 等,它更像是五个,但让我们保持简单)

    【讨论】:

    • 不,C++ 也会在这里发出callvirt。毕竟,这是一种虚拟方法。看我的回答。
    【解决方案4】:

    source code 有这样的评论:

    这是防止反向调用和其他调用者所必需的 不使用 callvirt 指令的人

    【讨论】:

      【解决方案5】:

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

      这将保存函数调用。除此之外,或许更好地理解 EqualsHelper 函数可能会阐明为什么需要这种优化。

      编辑:

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

      【讨论】:

        【解决方案6】:

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

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

        false

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

        【讨论】:

        • 这绝对是真的。这实际上是所有Equals 实现的典型样板代码。我的问题的真正实质是为什么要进行测试this != null。天真的断言是多余的,但是通过对 CLR 和 C# 编译器的深入了解,您可以理解并真正欣赏该实现。有关更多信息,请参阅接受的答案!
        猜你喜欢
        • 2020-01-04
        • 2011-01-21
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2011-08-27
        • 1970-01-01
        相关资源
        最近更新 更多