在 C# 中,`x is int?` 和 `x is int` 之间有区别吗?

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?;
}

上述两种方法在传递 null 引用或盒装 T 值时似乎表现相同。但是,生成的 MSIL 代码有点不同:

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

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

如您所见,o is T? 表达式实际上对 Nullable<T> 类型执行类型检查,尽管可空类型由 CLR 专门处理,因此 C# 表示装箱 T?值作为 null 参考(如果 T? 没有值)或盒装 T 值。似乎不可能在纯 C# 中甚至在 C++/CLI 中获得 Nullable<T> 类型的框(因为运行时处理 box 操作码来支持这个 "T? => T 框/null”拳击)。

我是不是遗漏了什么或者 o is T? 实际上等同于 C# 中的 o is T

根据规范(强调我的),在 E is T 中,T 的不可空值类型和相应的可空类型的处理方式相同:

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.

考虑这个通用方法:

static bool Is<T>(object arg)
{
    return arg is T;
}

此方法的关键部分被编译为 isinst !!T。现在您会期望 Is<int?>(arg) 的行为方式与 arg is int? 完全相同,不是吗?为了确保这种精确的一致性,C# 编译器必须在所有情况下发出相同的 CIL,并让 CLR 承担处理可空类型的负担。

可以在 GitHub: IsInst, ObjIsInstanceOf 的 coreclr 源代码中查看 CLR 的行为。正如您在第二个函数中看到的那样,如果类型是参数类型的可为 null 的表示形式,则它 returns true.

allow an object of type T to be cast to Nullable (they have the same representation)

是的,这些指令的当前行为是相同的,因此将 is T? 更改为 is T 不会有任何区别(即使对于 null 参数),但为了应对对于 CLR 未来可能发生的任何更改,C# 编译器无法做出该决定(尽管 isinst 行为更改的可能性接近于零)。

可空类型在 .NET 中确实是一件奇妙的事情,特别是由于它们在 CLR 中的特殊处理,尽管它们在 CIL 中没有特殊语法(为了兼容性)。确实没有将可空类型装箱到其实际类型而不是基础类型的正常方法,因为它会导致强制转换和检查中的不一致(空引用是否等于装箱的可空类型是否为 null?)。但是,您可以 trick CLR 认为您给它一个装箱的可空类型(不是您应该的)。