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

2

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

Nostomania answered 7/3, 2017 at 0:14 Comment(5)
That's a lot of up-votes for such a confusing question. What are you actually asking here? The answer to your title is plainly evident: the IL shows there is in fact a difference. But your question seems to instead be asking whether you can box a Nullable<T> object. Please fix your question so it's actually clear what you're trying to find out.Lessor
I'm asking if "o is T? is practically equivalent to o is T in C#", it is as clear as it can be :)Nostomania
Maybe you are looking for one of these: #3776082, #19055630, #1388097, or https://mcmap.net/q/520332/-why-nullable-lt-t-gt-is-a-structLessor
"Practically equivalent" in what way? What form of equivalence would not be "practical" in your view? I don't think M2() will ever return true, so it doesn't seem practical to me, nor equivalent.Lessor
Are you surprised that is in C# gets compiled to isinst in CIL for both types, on the basis that Nullable<T> is specially handled by the CLR? That is exactly the reason why it gets compiled to the same thing, so the C# compiler doesn't have to handle nullable shenanigans.Towery
M
7

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.
Manvell answered 7/3, 2017 at 0:32 Comment(0)
T
1

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).

Towery answered 9/3, 2017 at 12:11 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.