For example the following code demonstrates my line of thought:
class Program
{
static void Main(string[] args)
{
int i = 0;
IsNull(i); // Works fine
string s = null;
IsNull(s); // Blows up
}
static void IsNull<T>(T obj)
{
if (obj == null)
throw new NullReferenceException();
}
}
Also the following code:
int i = 0;
bool b = i == null; // Always false
Is there an implicit object cast going on? such that:
int i = 0;
bool b = (object)i == null;
Yes, obj
gets boxed by the compiler. This is the IL generated for your IsNull
function:
.maxstack 8
IL_0000: ldarg.0
IL_0001: box !!T
IL_0006: brtrue.s IL_000e
IL_0008: newobj instance void [mscorlib]System.NullReferenceException::.ctor()
IL_000d: throw
IL_000e: ret
The box
instruction is where the casting happens.
The compiler doesn't know anything specific about T
so it must assume that it must be an object
- the base type of everything in .NET; this is why it boxes obj
to make sure that the null check can be performed. If you use a type constraint you can give more information to the compiler about T
.
For example, if you use where T : struct
your IsNull
function won't compile anymore because the compiler knows T
is a value type and null is not a value for value types.
Boxing a value type instance always returns a valid (non-null) object instance*, so the IsNull
function would never throw for a value type. This is actually correct behavior if you think about it: the numeric value 0
is not null
- a value type value cannot possibly be null
.
In the code above brtrue.s
is very much like if(objref!=0)
- it doesn't check the value of the object (the value type value before boxing) because at the time of the check, it's not a value that's on top of the stack: it's the boxed object instance that's on top. Since that value (it's really a pointer) is non-null, the check for null
never comes back as true.
*Jon Hanna pointed out in a comment that this statement is not true for default(Nullable<T>)
which is correct - boxing this value returns null
for any T
.