xxbbcc's answer assumes that the OP is asking "why isn't 0 equal to null", which may well be what the question is all about. On the other hand, in the context of generic types, questions about boxing often have to do with the performance benefit that generic types offer by avoiding boxing.
In considering that question, the IL could be misleading. It includes a box instruction, but that doesn't mean that a boxed instance of the value type will actually be allocated on the heap. The IL "boxes" the value because the IL code is also also generic; the substitution of type arguments for type parameters is the responsibility of the JIT compiler. For a non-nullable value type, the JIT compiler optimizes away the IL instructions for boxing and checking the result because it knows that the result will always be non-null.
I added a Thread.Sleep call to the sample code, to give time to attach the debugger. (If you start the debugger in Visual Studio with F5, certain optimizations are disabled even if it is a release build). Here's the machine code in Release build:
Thread.Sleep(20000);
00000000 55 push ebp
00000001 8B EC mov ebp,esp
00000003 83 EC 0C sub esp,0Ch
00000006 89 4D FC mov dword ptr [ebp-4],ecx
00000009 83 3D 04 0B 4E 00 00 cmp dword ptr ds:[004E0B04h],0
00000010 74 05 je 00000017
00000012 E8 AD 4B 6A 71 call 716A4BC4
00000017 33 D2 xor edx,edx
00000019 89 55 F4 mov dword ptr [ebp-0Ch],edx
0000001c 33 D2 xor edx,edx
0000001e 89 55 F8 mov dword ptr [ebp-8],edx
00000021 B9 C8 00 00 00 mov ecx,0C8h
00000026 E8 45 0E 63 70 call 70630E70
int i = 0;
0000002b 33 D2 xor edx,edx
0000002d 89 55 F8 mov dword ptr [ebp-8],edx
IsNull(i); // Works fine
00000030 8B 4D F8 mov ecx,dword ptr [ebp-8]
00000033 FF 15 E4 1B 4E 00 call dword ptr ds:[004E1BE4h]
string s = null;
00000039 33 D2 xor edx,edx
0000003b 89 55 F4 mov dword ptr [ebp-0Ch],edx
IsNull(s); // Blows up
0000003e 8B 4D F4 mov ecx,dword ptr [ebp-0Ch]
00000041 BA 50 1C 4E 00 mov edx,4E1C50h
00000046 FF 15 24 1C 4E 00 call dword ptr ds:[004E1C24h]
}
0000004c 90 nop
0000004d 8B E5 mov esp,ebp
0000004f 5D pop ebp
00000050 C3 ret
Note that the call instruction has a different target for the int and the string. Here they are:
if (obj == null)
00000000 55 push ebp
00000001 8B EC mov ebp,esp
00000003 83 EC 0C sub esp,0Ch
00000006 33 C0 xor eax,eax
00000008 89 45 F8 mov dword ptr [ebp-8],eax
0000000b 89 45 F4 mov dword ptr [ebp-0Ch],eax
0000000e 89 4D FC mov dword ptr [ebp-4],ecx
00000011 83 3D 04 0B 32 00 00 cmp dword ptr ds:[00320B04h],0
00000018 74 05 je 0000001F
0000001a E8 ED 49 6E 71 call 716E4A0C
0000001f B9 70 C7 A4 70 mov ecx,70A4C770h
00000024 E8 2F FA E9 FF call FFE9FA58
00000029 89 45 F8 mov dword ptr [ebp-8],eax
0000002c 8B 45 F8 mov eax,dword ptr [ebp-8]
0000002f 8B 55 FC mov edx,dword ptr [ebp-4]
00000032 89 50 04 mov dword ptr [eax+4],edx
00000035 8B 45 F8 mov eax,dword ptr [ebp-8]
00000038 85 C0 test eax,eax
0000003a 75 1D jne 00000059
throw new NullReferenceException();
0000003c B9 98 33 A4 70 mov ecx,70A43398h
00000041 E8 12 FA E9 FF call FFE9FA58
00000046 89 45 F4 mov dword ptr [ebp-0Ch],eax
00000049 8B 4D F4 mov ecx,dword ptr [ebp-0Ch]
0000004c E8 DF 22 65 70 call 70652330
00000051 8B 4D F4 mov ecx,dword ptr [ebp-0Ch]
00000054 E8 BF 2A 57 71 call 71572B18
}
00000059 90 nop
0000005a 8B E5 mov esp,ebp
0000005c 5D pop ebp
0000005d C3 ret
and
if (obj == null)
00000000 55 push ebp
00000001 8B EC mov ebp,esp
00000003 83 EC 0C sub esp,0Ch
00000006 33 C0 xor eax,eax
00000008 89 45 F8 mov dword ptr [ebp-8],eax
0000000b 89 45 F4 mov dword ptr [ebp-0Ch],eax
0000000e 89 4D FC mov dword ptr [ebp-4],ecx
00000011 83 3D 04 0B 32 00 00 cmp dword ptr ds:[00320B04h],0
00000018 74 05 je 0000001F
0000001a E8 ED 49 6E 71 call 716E4A0C
0000001f B9 70 C7 A4 70 mov ecx,70A4C770h
00000024 E8 2F FA E9 FF call FFE9FA58
00000029 89 45 F8 mov dword ptr [ebp-8],eax
0000002c 8B 45 F8 mov eax,dword ptr [ebp-8]
0000002f 8B 55 FC mov edx,dword ptr [ebp-4]
00000032 89 50 04 mov dword ptr [eax+4],edx
00000035 8B 45 F8 mov eax,dword ptr [ebp-8]
00000038 85 C0 test eax,eax
0000003a 75 1D jne 00000059
throw new NullReferenceException();
0000003c B9 98 33 A4 70 mov ecx,70A43398h
00000041 E8 12 FA E9 FF call FFE9FA58
00000046 89 45 F4 mov dword ptr [ebp-0Ch],eax
00000049 8B 4D F4 mov ecx,dword ptr [ebp-0Ch]
0000004c E8 DF 22 65 70 call 70652330
00000051 8B 4D F4 mov ecx,dword ptr [ebp-0Ch]
00000054 E8 BF 2A 57 71 call 71572B18
}
00000059 90 nop
0000005a 8B E5 mov esp,ebp
0000005c 5D pop ebp
0000005d C3 ret
Looks more or less the same, right? But here's what you get if you start the process first and then attach the debugger:
Thread.Sleep(20000);
00000000 55 push ebp
00000001 8B EC mov ebp,esp
00000003 50 push eax
00000004 B9 20 4E 00 00 mov ecx,4E20h
00000009 E8 6A 0C 67 71 call 71670C78
IsNull(s); // Blows up
0000000e B9 98 33 A4 70 mov ecx,70A43398h
00000013 E8 6C 20 F9 FF call FFF92084
00000018 89 45 FC mov dword ptr [ebp-4],eax
0000001b 8B C8 mov ecx,eax
0000001d E8 66 49 6C 70 call 706C4988
00000022 8B 4D FC mov ecx,dword ptr [ebp-4]
00000025 E8 46 51 5E 71 call 715E5170
0000002a CC int 3
Not only has the optimizer removed the boxing of the value type, it has inlined the call to the IsNull method for the value type by removing it altogether. It's not obvious from the above machine code, but the call to IsNull for the reference type is also inlined. The call 706C4988
instruction seems to be the NullReferenceException constructor, and call 715E5170
seems to be the throw
.