为什么要检查此!= null?


72

有时,我喜欢花一些时间查看.NET代码,以了解幕后如何实现事情。我在String.Equals通过Reflector查看方法时偶然发现了这颗宝石。

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

白介素

.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?我必须假设有目的,否则到现在可能已经捕获并删除了它。


1
您也可以看看EqualsHelper吗?似乎他们想使用EqualsHelper,但它可能无法按照他们想要的方式处理null值。
Danny T.

这是特别有趣,因为文件明确规定,等于将抛出一个NullReferenceException如果实例为null ....
womp

我的猜测是这是疏忽,还是与EqualsHelper工作方式有关。我完全看不到该if语句的必要性,假设EqualsHelperfalsestrBisnullthisnot时返回。但也许我只是不够聪明,无法理解:)
Nate Pinchot 2010年

@womp-在4.0中,第一行是if(this == null) throw new NullReferenceException()正确的。
约翰·拉施(John Rasch)2010年

11
这段代码是在1999年12月13日C#团队致力于实现空值检查的那一天之前编写的。 blogs.msdn.com/b/ericgu/archive/2008/07/02/...
汉斯帕桑特

Answers:


85

我假设您正在查看.NET 3.5的实现?我相信.NET 4的实现略有不同。

但是,我有一个偷偷的怀疑,这是因为甚至有可能在null引用上非虚拟地调用虚拟实例方法。即在IL中可能。我看看是否可以产生一些IL来调用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,这是我们通常不会期望的。

足够的字符串...让我们尝试自己实现引用相等,看看是否可以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;
    }
}

相同的步骤,之前-拆机,换callvirtcall,重新组装,然后看着它打印true...

请注意,尽管另一个答案引用了这个C ++问题,但我们在这里更加曲解……因为我们非虚拟地调用虚拟方法。通常,即使C ++ / CLI编译器也将使用callvirt虚拟方法。换句话说,我认为在这种特殊情况下,唯一的为thisnull的方法是手动编写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)

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

如果你走到今天这一步,你可能也想看看我的博客文章约值类型如何可以声明参数构造函数...在IL。


我在这台机器上没有.NET 4。看看该版本中的实现看起来会很有趣。有趣的假设。让我们知道你发现什么。
布莱恩·吉迪恩

1
迷人。因此,铁杆IL程序员可能会使用我开发不正确的API,而我唯一能防止的就是检查this != null每个实例方法?只要考虑说所说的库与安全性有关的含义!
Brian Gideon 2010年

12
怪异的乔恩·斯基特。我知道您疯了,擅长所有事情,但这简直是不可思议。我会戴上帽子给你,但我没有戴。取而代之的是,您厚颜无耻。
白金Azure

1
@Brian,我的猜测是,抖动为.NET4中的调用添加了对非null arg0的检查,而.NET2仅对callvirt执行了此检查(绝对有必要,因为它需要获取虚拟方法表。 )
丹·布莱恩特2010年

@丹:不,我相信对非空性的检查是在.NET 4代码的IL中执行的,这是我们没有看到它的唯一原因。
乔恩·斯基特

17

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

听起来吓人吗?确实有点。但是,大多数编译器都确保不会发生这种情况。.call指令仅在以下情况下输出:null(我很确定C#始终使用callvirt)。

但是,并非所有语言都适用,并且出于某种原因,我不完全知道BCL团队选择System.String在这种情况下进一步加强该类。

可以弹出的另一种情况是反向呼叫。


9

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

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


不,C ++也会callvirt在这里发出。毕竟,这是一种虚拟方法。看我的答案。
乔恩·斯基特


1

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

这将保存一个函数调用。除此之外,对EqualsHelper功能的更好理解也许可以为为什么需要这种优化提供一些启示。

编辑:

嗯,所以EqualsHelper函数接受a(string, string)作为参数。如果strB为null,则从根本上意味着它要么是一个空对象,要么无法成功地转换为字符串。 如果为strBnull的原因是该对象是无法转换为字符串的其他类型,则您不希望使用本质上两个null值调用EqualsHelper(它将返回true)。在这种情况下 ,Equals函数返回false。因此,此if语句不仅仅是优化,它实际上还确保了适当的功能。


0

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

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

写道false

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


那绝对是真的。那实际上是所有Equals实现中典型的样板代码。我的问题的实质是为什么要进行测试this != null。天真的断言是它是多余的,但是凭借对CLR和C#编译器的深厚知识,您可以理解并真正欣赏其实现。有关更多信息,请参见接受的答案!
Brian Gideon
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.