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

发布于 2024-09-07 03:57:39 字数 1257 浏览 8 评论 0原文

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

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

Occasionally I like to spend some time looking at the .NET code just to see how things are implemented behind the scenes. I stumbled upon this gem while looking at the String.Equals method via 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);
}

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 
}

What is the reasoning for checking this against null? I have to assume there is purpose otherwise this probably would have been caught and removed by now.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(6

旧情别恋 2024-09-14 03:57:39

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

但是,我偷偷怀疑这是因为甚至可以在空引用上非虚拟地调用虚拟实例方法。也就是说,在伊利诺伊州是可能的。我将看看是否可以生成一些调用 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< /code> 来调用,重新组装,并观察它打印true...

请注意,虽然另一个答案引用了这个 C++ 问题,我们在这里更加狡猾......因为我们正在调用一个虚拟 非虚拟方法。通常,即使是 C++/CLI 编译器也会使用 callvirt 作为虚拟方法。换句话说,我认为在这种特殊情况下,使 this 为 null 的唯一方法是手动编写 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 中声明无参数构造函数...。

I assume you were looking at the .NET 3.5 implementation? I believe the .NET 4 implementation is slightly different.

However, I have a sneaking suspicion that this is because it's possible to call even virtual instance methods non-virtually on a null reference. Possible in IL, that is. I'll see if I can produce some IL which would call null.Equals(null).

EDIT: Okay, here's some interesting code:

.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

I got this by compiling the following C# code:

using System;

class Test
{
    static void Main()
    {
        string x = null;
        Console.WriteLine(x.Equals(null));

    }
}

... and then disassembling with ildasm and editing. Note this line:

IL_0005:  call instance bool [mscorlib]System.String::Equals(string)

Originally, that was callvirt instead of call.

So, what happens when we reassemble it? Well, with .NET 4.0 we get this:

Unhandled Exception: System.NullReferenceException: Object
reference not set to an instance of an object.
    at Test.Main()

Hmm. What about with .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()

Now that's more interesting... we've clearly managed to get into EqualsHelper, which we wouldn't have normally expected.

Enough of string... let's try to implement reference equality ourselves, and see whether we can get null.Equals(null) to return 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;
    }
}

Same procedure as before - disassemble, change callvirt to call, reassemble, and watch it print true...

Note that although another answers references this C++ question, we're being even more devious here... because we're calling a virtual method non-virtually. Normally even the C++/CLI compiler will use callvirt for a virtual method. In other words, I think in this particular case, the only way for this to be null is to write the IL by hand.


EDIT: I've just noticed something... I wasn't actually calling the right method in either of our little sample programs. Here's the call in the first case:

IL_0005:  call instance bool [mscorlib]System.String::Equals(string)

here's the call in the second:

IL_0005:  call instance bool [mscorlib]System.Object::Equals(object)

In the first case, I meant to call System.String::Equals(object), and in the second, I meant to call Test::Equals(object). From this we can see three things:

  • You need to be careful with overloading.
  • The C# compiler emits calls to the declarer of the virtual method - not the most specific override of the virtual method. IIRC, VB works the opposite way
  • object.Equals(object) is happy to compare a null "this" reference

If you add a bit of console output to the C# override, you can see the difference - it won't be called unless you change the IL to call it explicitly, like this:

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

So, there we are. Fun and abuse of instance methods on null references.

If you've made it this far, you might also like to look at my blog post about how value types can declare parameterless constructors... in IL.

情愿 2024-09-14 03:57:39

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

听起来很可怕吗?确实是有一点。然而大多数编译器都会确保这种情况永远不会发生。仅当 null 不可能时才会输出 .call 指令(我非常确定 C# 始终使用 callvirt)。

但并非所有语言都如此,并且由于我不太清楚 BCL 团队选择在此实例中进一步强化 System.String 类的原因,情况并非如此。

另一种可能弹出此消息的情况是反向 pinvoke 调用。

The reason why is that it is indeed possible for this to be null. There are 2 IL op codes which can be used to invoke a function: call and callvirt. The callvirt function causes the CLR to perform a null check when invoking the method. The call instruction does not and hence allows for a method to be entered with this being null.

Sound scary? Indeed it is a bit. However most compilers ensure this doesn't ever happen. The .call instruction is only ever outputted when null is not a possibility (I'm pretty sure that C# always uses callvirt).

This isn't true for all languages though and for reasons I don't exactly know the BCL team chose to further harden the System.String class in this instance.

Another case where this can popup is in reverse pinvoke calls.

输什么也不输骨气 2024-09-14 03:57:39

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

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

The short answer is that languages like C# force you to create an instance of this class before calling the method, but the Framework itself does not. There are two¹ different ways in CIL to call a function: call and callvirt.... Generally speaking, C# will always emit callvirt, which requires this to not be null. But other languages (C++/CLI comes to mind) could emit call, which doesn't have that expectation.

(¹okay, it's more like five if you count calli, newobj etc, but let's keep it simple)

原谅我要高飞 2024-09-14 03:57:39

源代码有以下评论:

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

The source code has this comment:

this is necessary to guard against reverse-pinvokes and other callers
who do not use the callvirt instruction

携君以终年 2024-09-14 03:57:39

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

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

编辑:

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

Let's see... this is the first string you're comparing. obj is the second object. So it looks like it's an optimization of sorts. It's first casting obj to a string type. And if that fails, then strB is null. And if strB is null while this isn't, then they're definitely not equal and the EqualsHelper function can be skipped.

That will save a function call. Beyond that, perhaps a better understanding of the EqualsHelper function might shed some light on why this optimization is needed.

EDIT:

Ah, so the EqualsHelper function is accepting a (string, string) as parameters. If strB is null, then that essentially means that it was either a null object to begin with, or it couldn't successfully be cast into a string. If the reason for strB being null is that the object was a different type that couldn't be converted to a string then you wouldn't want to call EqualsHelper with essentially two null values (that'll return true). The Equals function should return false in this case. So this if statement is more than an optimization, it actually ensures proper functionality as well.

吾性傲以野 2024-09-14 03:57:39

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

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

写入 false

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

If the argument (obj) does not cast to a string then strB will be null and the result should be false. Example:

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

writes false.

Remember that string.Equals() method is called for any argument type, not only for other strings.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文