问题 为什么IsAssignableFrom在将nullable与接口进行比较时返回false?


C#中的以下调用返回false:

typeof(IComparable).IsAssignableFrom(typeof(DateTime?))

但是,以下行完全有效:

IComparable comparable = (DateTime?)DateTime.Now;

为什么会这样?

是因为使用了可空类型 Nullable<T>并且第一个泛型参数实现接口的事实并不意味着Nullable类也实现了该接口? (例如: List<Foo> 没有实现接口 Foo 实行)

编辑: 我认为上面的行是编译的,因为在装入可空类型时,只有底层类型被装箱,如下所述: https://msdn.microsoft.com/en-us/library/ms228597.aspx


2007
2017-10-10 10:04


起源

有趣的问题是为什么你的第二行编译即使 Nullable<T> 没有实现 IComparable - Tim Schmelter
我应该重写这个问题。 - tigrou
您提供的链接已经很好地解释了它,甚至显示了一个可以分配的可为空的示例 IConvertible。所以原因是,如果一个可空值有一个值,它将被装箱到底层类型 T,而不是可空的。 - Tim Schmelter


答案:


这种行为的原因是 IsAssignableFrom() 不考虑编译器为可空类型的转换发出的特殊装箱转换。

请注意,您实际上并不需要在问题中使用强制转换。

代替

 IComparable comparable = (DateTime?)DateTime.Now;

你可以写:

DateTime? test = DateTime.Now;
IComparable comparable = test;

这些行中的第一行编译是因为 Nullable<T> 提供隐式转换运算符:

public static implicit operator Nullable<T> (
    T value
)

第二行导致编译器发出一个box指令:

L_000e: box [mscorlib]System.Nullable`1<valuetype [mscorlib]System.DateTime>

这个装箱操作由C#语言规范的第6.1.7节涵盖, 拳击转换 (这包括专门针对可空类型的装箱转换),其中指出:

装箱转换允许将值类型隐式转换为   参考类型。任何一个拳击转换都存在   对象和动态的非可空值类型到System.ValueType 和   到非可空值类型实现的任何接口类型。   此外,枚举类型可以转换为System.Enum类型。

如果存在从可空类型到引用类型的装箱转换   并且仅当底层存在拳击转换时   非可空值类型到引用类型。

如果值类型具有装箱转换,则它具有到接口类型I的装箱转换   到接口类型I0和I0有一个身份转换为I.

值类型具有到接口类型I的装箱转换(如果它具有)   拳击转换为接口或委托类型I0和I0是   方差可转换(§13.1.3.2)到I.

拳击非可空值类型的值包括分配对象实例和   将值类型值复制到该实例中。结构可以装箱   到System.ValueType类型,因为它是所有的基类   结构(第11.3.2节)。

这就是导致上面拳击行动的原因。我用粗体和斜体来表达最相关的观点。

另请参阅此链接(由OP提供): https://msdn.microsoft.com/en-us/library/ms228597.aspx


11
2017-10-10 10:28



但是为什么在我不投票的时候使用它 T 一个 Nullable<T> 但是当我将它分配给一个接口时 T 工具?所以它确实回答了我可以使用的问题 DateTime.Now 并将其分配给 DateTime? 变量,但它没有解释(?)为什么我可以为一个接口分配一个可空 T 实现。 - Tim Schmelter
@TimSchmelter确实,这是一个不同的问题。 :) - Matthew Watson
这不是一个真正不同的问题,因为只有当OP尝试时,你的答案才能解释它: typeof(DateTime).IsAssignableFrom(typeof(DateTime?));(也将返回 false)。但实际上他正试图将其分配给 IComparable - Tim Schmelter
实际上OP已经回答了这个问题 链接 他提供了。具有值的Nullables将被装箱到底层类型 T 不是可空的。 - Tim Schmelter
@TimSchmelter我添加了对C#标准相关部分的引用;我还将添加OP的完整性链接。 - Matthew Watson


Nullables很特别。

object boxedNullable = new decimal?(42M);

Console.WriteLine(boxedNullable.GetType().Name); // Decimal

当你打开一个可以为空的值时,你实际做的是打包底层值,而不是可空。所以 default(decimal?) 会给你的 null (而不是“无价值可空”),和 new decimal?(42M) 会给你一个盒装 decimal

将值类型转换为接口时,它 必须盒装 - 所以你的第二行实际上将可空的nullable改为盒装 DateTimeDateTime? 没有实现 IComparable但是 DateTime 确实 - 这就是你最终投射到界面的内容,因为必须首先装箱值类型。

这在ECMA规范中定义, I.8.2.4 Boxing and unboxing of values

所有值类型都有一个名为box的操作。装箱任何值类型的值会产生其盒装值;即,包含原始值的按位副本的相应盒装类型的值。如果值类型是可空类型 - 定义为值类型的实例化 System.Nullable<T> - 结果是类型为T的Value属性的空引用或按位副本,具体取决于其HasValue属性(分别为false和true)。所有盒装类型都有一个名为unbox的操作,它会生成一个指向值的位表示形式的托管指针。

你最好的选择是使用其中之一 x is IComparable 要么 x as IComparable 找到某种类型的实现 IComparable;使用反射让你暴露出很多.NET和C#的小怪癖(以及编写代码的人使用的其他任何语言)。


3
2017-10-10 10:37