class C<T> where T : struct {
bool M1(object o) => o is T;
bool M2(object o) => o is T?;
}
The two methods above seems to behave equally, both when passing null reference or boxed T value. However, the generated MSIL code is a bit different:
.method private hidebysig instance bool M1(object o) cil managed {
.maxstack 8
IL_0000: ldarg.1
IL_0001: isinst !T
IL_0006: ldnull
IL_0007: cgt.un
IL_0009: ret
}
vs
.method private hidebysig instance bool M2(object o) cil managed {
.maxstack 8
IL_0000: ldarg.1
IL_0001: isinst valuetype [mscorlib]System.Nullable`1<!T>
IL_0006: ldnull
IL_0007: cgt.un
IL_0009: ret
}
As you may see, the o is T? expression actually performs type check for Nullable<T> type, despite the fact that nullable types are specially handled by CLR so that C# represents boxed T? value as null reference (if T? has no value) or boxed T value. It seems impossible to get box of Nullable<T> type in pure C# or maybe even in C++/CLI (since runtime handles box opcode to support this "T? => T box / null" boxing).
Am I missing something or o is T? is practically equivalent to o is T in C#?
According to the spec (emphasis mine), in E is T, non-nullable value types of T and corresponding nullable types are handled the same way:
7.10.10 The
isoperatorThe
isoperator is used to dynamically check if the run-time type of an object is compatible with a given type. The result of the operationE is T, whereEis an expression andTis a type, is a boolean value indicating whetherEcan successfully be converted to typeTby a reference conversion, a boxing conversion, or an unboxing conversion. The operation is evaluated as follows, after type arguments have been substituted for all type parameters:
If
Eis an anonymous function, a compile-time error occursIf
Eis a method group or the null literal, of if the type ofEis a reference type or a nullable type and the value of E is null, the result is false.Otherwise, let
Drepresent the dynamic type ofEas follows:
- If the type of
Eis a reference type,Dis the run-time type of the instance reference byE.If the type of
Eis a nullable type,Dis the underlying type of that nullable type.If the type of
Eis a non-nullable value type,Dis the type ofE.The result of the operation depends on
DandTas follows:
- If
Tis a reference type, the result is true ifDandTare the same type, ifDis a reference type and an implicit reference conversion fromDtoTexists, or ifDis a value type and a boxing conversion fromDtoTexists.- If
Tis a nullable type, the result is true ifDis the underlying type ofT.- If
Tis a non-nullable value type, the result is true ifDandTare the same type.- Otherwise, the result is false.
Consider this generic method:
static bool Is<T>(object arg)
{
return arg is T;
}
The crucial part of this method gets compiled to isinst !!T. Now you would expect Is<int?>(arg) to behave exactly the same way as arg is int?, wouldn't you? To ensure this exact consistency, the C# compiler must emit the same CIL in all the cases and let the burden of handling nullable types lie on the CLR.
The behaviour of the CLR can be viewed in the coreclr source code on GitHub: IsInst, ObjIsInstanceOf. As you can see in the second function, if the type is a nullable representation of the argument type, it returns true.
allow an object of type T to be cast to Nullable (they have the same representation)
Yes, the current behaviour of these instructions is the same, so changing is T? to is T won't make any difference (even for null argument), but to cope with any possible future changes in the CLR, the C# compiler cannot make that decision (although the probability of isinst behaviour changed is close to zero).
Nullable types are really a wondrous thing in .NET, especially due to their particular handling in the CLR, despite the fact that they have no special syntax in CIL (for compatibility). There is really no normal way of boxing a nullable type to its actual type and not the underlying one, as it would raise inconsistencies in casts and checks (is null reference equal to a boxed nullable type null or not?). However, you can trick the CLR into thinking you give it a boxed nullable type (not that you should).