Why is this needed? = Null?

Sometimes I like to spend some time looking at .NET code to see how things are implemented behind the scenes. I came across this stone while looking at the String.Equals method via Reflector.

FROM#

 [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 reason for checking this for null ? I have to assume that there is a goal, otherwise it would probably be caught and removed by now.

+71
c # clr reflector
Jun 29 '10 at 18:11
source share
6 answers

I assume you were considering implementing .NET 3.5? I find the implementation of .NET 4 is a little different.

However, I have a suspicious suspicion that this is due to the fact that you can even call virtual instance methods with virtually no null reference. Perhaps in IL, that is. I will see if I can create an IL that will call null.Equals(null) .

EDIT: Ok, here is 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 using ildasm and editing. Pay attention to this line:

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

Originally it was callvirt instead of call .

So what happens when we collect it? Well, with .NET 4.0 we get the following:

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

Hm. What's 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, more interestingly ... we obviously managed to get into EqualsHelper , which we usually did not expect.

Enough of the line ... try to implement referential equality yourself and see if 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; } } 

The same procedures as before - to parse, change callvirt to call , collect and see its print true ...

Note that while the other answers this question in C ++ , we are even more insidious here ... because we invoke the virtual method in practice. Usually even a C ++ / CLI compiler will use callvirt for a virtual method. In other words, I think that in this particular case, the only way for this be null is to manually write IL.




EDIT: I just noticed something ... In fact, I did not name the correct method in any of our small sample programs. Here's the call in the first case:

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

here is the call in the second:

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

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

  • You need to be careful with overload.
  • The C # compiler emits calls to the virtual method declarator - not the most concrete redefinition of the virtual method. IIRC, VB works in reverse.
  • object.Equals(object) glad to compare the null reference "this"

If you add a little console output to the C # override, you will see the difference - it will not be called unless you change the IL to call it explicitly, for example:

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

So here we are. Fun and abuse of instance methods on null links.

If you have done this this far, you might also like my blog post about how value types can be declared by constructors without parameters ... in IL.

+85
Jun 29 '10 at 18:20
source share

The reason is that null really possible for this . There are two IL IL codes that you can use to call a function: call and callvirt. The callvirt function forces the CLR to perform a null check when the method is called. The call statement does not allow and, therefore, allows you to enter a method with this null .

Does that sound scary? In fact, this is a bit. However, most compilers guarantee that this will never happen. The .call instruction is only output when null not an option (I'm sure C # always uses callvirt).

This is not true for all languages, although for reasons that I don't know for sure, the BCL team decided to further strengthen the System.String class in this instance.

Another case where this popup calls pinvoke callbacks.

+17
Jun 29 '10 at 18:24
source share

The short answer is that languages ​​like C # force you to instantiate this class before calling the method, but the platform itself does not. There are two different ways in CIL: the call and callvirt functions .... Generally speaking, C # will always allocate callvirt , for which this will not be null. But other languages ​​(C ++ / CLI comes to mind) may emit a call that does not have this expectation.

(ΒΉokay, this is more like five if you consider calli, newobj, etc., but let it simplify)

+9
Jun 29 '10 at 18:24
source share

There is this comment in the source code :

this is necessary to protect against callback and other callers who do not use the callvirt command

+4
Jun 13 '14 at 3:31 on
source share

Let's see ... this is the first line you are comparing. obj is the second object. So this is like optimization. First, it throws obj onto the string type. And if that fails, then strB is null. And if strB is null, but this is not, then they are definitely not equal and the EqualsHelper function may be skipped.

This will save the function call. In addition, perhaps a better understanding of the EqualsHelper function may shed light on why this optimization is necessary.

EDIT:

Ah, therefore, the EqualsHelper function takes parameters (string, string) as parameters. If strB is NULL, this means that it was either a null object or it could not be successfully wrapped to a string. If the reason for strB is null, that the object was a different type that could not be converted to a string, then you would not want to call EqualsHelper with essentially two null values ​​(which will return true). The Equals function should return false in this case. Thus, the if statement is more than optimization; it also provides the proper functionality.

+1
Jun 29 '10 at 18:22
source share

If the argument (obj) is not passed to the 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 the string.Equals () method is called for any type of argument, and not just for other strings.

0
Jun 30 '10 at 19:32
source share



All Articles