Search code examples
c#typesnullablecilboxing

Is there a difference between `x is int?` and `x is int` in C#?


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#?


Solution

  • 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 is operator

    The is operator is used to dynamically check if the run-time type of an object is compatible with a given type. The result of the operation E is T, where E is an expression and T is a type, is a boolean value indicating whether E can successfully be converted to type T by 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 E is an anonymous function, a compile-time error occurs

    • If E is a method group or the null literal, of if the type of E is a reference type or a nullable type and the value of E is null, the result is false.

    • Otherwise, let D represent the dynamic type of E as follows:

      • If the type of E is a reference type, D is the run-time type of the instance reference by E.
      • If the type of E is a nullable type, D is the underlying type of that nullable type.

      • If the type of E is a non-nullable value type, D is the type of E.

    • The result of the operation depends on D and T as follows:

      • If T is a reference type, the result is true if D and T are the same type, if D is a reference type and an implicit reference conversion from D to T exists, or if D is a value type and a boxing conversion from D to T exists.
      • If T is a nullable type, the result is true if D is the underlying type of T.
      • If T is a non-nullable value type, the result is true if D and T are the same type.
      • Otherwise, the result is false.