Checking generic parameters for Nothing/null

Consider the following code fragment:

Module Module1
Sub Foo(Of T)(ByVal x As T)
If x Is Nothing Then

End If
End Sub
Sub Main()
End Sub
End Module

The first thing you might ask is : "hey, how come the compiler allows me to check for Nothing? Isn't that a reference comparison? There is no constraint on T, so why does the compiler allow you to do this?" The simple answer is : it makes your life easier. Imagine that the compiler did not allow this : you now have to write two versions of every generic method : one that assumes the parameter can be Nothing and thus must make a null-check, and one that assumes the parameter must have a value. That really sucks.

Ok, but what code does the compiler generate? Here's the bit of IL code that's interesting:

IL_0001:  ldarg.0
IL_0002: box !!T
IL_0007: ldnull
IL_0008: ceq

Ah ha! The compiler treats x as a value type, and emits a box instruction. The it emits code that checks against null : since we've now boxed x, this pushes two reference types onto the stack for ceq to operate on.

One way therefore to think about this, is that when a valuetype is passed to a method that takes a generic argument which is not constrained, the compiler will emit box instructions to make them reference types. Indeed, if you take a look at the number of managed objects, you'll definitely see that when you call Foo with a value type, an extra object is created on the heap (to see this, use the !dumpheap instruction in Visual Studio with SOS loaded - check this for more information). But when you call Foo with a reference type, no object is created on the heap.

Why does this happen? Let's take a look at how the CLR jits the above IL.

Here's the retail x86:

00000000  push        edi  
00000001 push esi
00000002 mov edi,ecx
00000004 cmp dword ptr ds:[007795E0h],0
0000000b je 00000012
0000000d call 79826E47
00000012 mov ecx,79102290h
00000017 call FFE60B1C
0000001c mov esi,eax
0000001e mov dword ptr [esi+4],edi
00000021 test esi,esi
00000023 jne 0000003B

There, definitely, the first call is constructing the boxed value, and the second call is calling the managed helper to test object equivalence.

Here's the version of retail x86 that's generated when Foo is called with a reference type:

00000000  push        esi  
00000001 mov esi,ecx
00000003 cmp dword ptr ds:[007795E0h],0
0000000a je 00000011
0000000c call 79826D47
00000011 test esi,esi
00000013 jne 0000001C

See, no extra call to construct an object on the heap.

Next time, I hope to post about some of the changes we are considering making for the next release.


Skip to main content