为什么不能直接取消装箱到可以显式转换为的类型?

时间:2019-09-04 14:34:25

标签: c# .net

今天将此作为一些EF / DB代码的一部分,并感到羞耻地说我以前从未见过。

在.NET中,您可以在类型之间进行显式转换。例如

int x = 5;
long y = (long)x;

您可以对对象进行装箱并取消装箱到该原始类型

int x = 5; 
object y = x;
int z = (int)y;

但是您不能直接将其拆箱为可以明确转换为的类型

int x = 5;
object y = x;
long z = (long)y;

尽管我直到今天才真正碰到它,但这实际上是记录在案的行为。 https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/types/boxing-and-unboxing#example-1

  

要使值类型的拆箱在运行时成功,要拆箱的项目必须是对以前通过对该值类型的实例进行装箱创建的对象的引用。尝试对null取消装箱会导致NullReferenceException。尝试取消对不兼容的值类型的引用的装箱会导致InvalidCastException。

我很好奇,出于某种技术原因,为什么运行时无法/不支持这种操作?

2 个答案:

答案 0 :(得分:1)

强制转换语法(Foo)bar C#执行以下操作之一:

  • 将参考类型转换为另一种参考类型
  • 将值类型转换为另一种值类型
  • 输入值类型
  • 取消装箱值类型的装箱

这些操作在语义上非常不同。将它们视为四个不同的操作确实更有意义,这四个操作由于历史原因碰巧共享相同的(Foo)bar语法。特别是,它们对在编译时需要知道哪些信息有不同的约束:

  • 拆箱操作需要知道拆箱值的类型
  • 值类型转换需要同时了解源类型和目标类型。

基本上是因为编译器需要在编译时知道要分配给值的字节数。在您的示例中,装箱的值为int的信息在编译时不可用,这意味着拆箱和向long的转换都无法编译。

这里有悖常理的是,相同的约束不适用于引用类型。实际上,强制转换引用类型的全部目的是编译器在编译时不知道确切的类型。您可以在更了解编译器之后使用强制转换,然后编译器接受该强制转换,然后在运行时进行类型检查以确保强制转换有效。

之所以可能是由于引用类型之间的一些根本差异:

  • 引用类型实例在运行时知道其自己的确切类型。它作为实例数据的一部分存储。
  • 引用类型是多态的,这意味着编译器不需要知道确切的实例类型。所有引用的大小均相同,因此分配多少字节没有任何歧义。

这些类型转换之间的语义差异意味着它们必须在不影响安全性的前提下进行合并。

让我们说C#支持在单个强制转换表达式中进行拆箱和转换:

int x = 70000;
object y = x;
short z = (short)y;

当前,取消装箱强制转换表示您希望装箱的值属于给定类型。如果不是这种情况,则会引发异常,因此您会发现该错误。但是使用强制转换语法的值类型转换表明您知道类型不同,并且转换可能会导致数据丢失。

如果该语言会自动取消装箱并进行转换,那么您将无法表达是否希望安全地取消装箱 而不会丢失任何数据。

答案 1 :(得分:0)

我不知道我可以简要总结关于这个问题的所有Eric Lippert's article,但是下面我发现与您的问题特别相关的文章部分;

  

“ [有] ... C#编译器认为是的某些转换   表示更改实际上被CLR验证程序视为   保持代表性。例如,从int到   uint被CLR视为保留表示形式,因为32   有符号整数的位可以重新解释为无符号整数   不改变位。这些情况可能微妙而复杂,并且   通常会影响与协方差相关的问题。

     

我也忽略了涉及泛型类型参数的转换   在编译时不知道是引用还是值类型。那里   是对那些主要分类的特殊规则   题外话。

     

无论如何,我们可以想到保留表示的转换   引用类型,例如那些保留了身份的转化   物体。当您将B投射到D时,您并未对   现有物体;您只是在验证它实际上是该类型   你说的是,然后继续前进。对象和位的身份   代表参考的保持不变。但是当你转换一个整数   翻一番,所得到的位是非常不同的。”