运行时如何知道装箱值类型的确切类型?

8
我了解什么是装箱。值类型被封装为对象/引用类型,然后作为对象存储在托管堆上。但我无法理解拆箱。
拆箱将您的对象/引用类型转换回值类型。
int i = 123;          // A value type
object box = i;       // Boxing
int j = (int)box;     // Unboxing

好的。但是如果我试图将一个值类型拆箱成另一个值类型,例如上面的示例中的long,它会抛出InvalidCastException异常。

long d = (long)box;

我认为运行时隐式地知道了“box”对象内包装的值类型的实际类型。如果我是正确的,我想知道这个类型信息存储在哪里。

编辑:

由于int可以隐式转换为long,这让我感到困惑。

int i = 123;
long lng = i;

这是完全没涉及到装箱/拆箱的技术,所以很好。


那 box.GetType() 怎么样? - speti43
2
运行时了解每个对象的类型。装箱值并没有什么不同。 - Jonathon Reinhart
假设你有一个值类型的“球”,你把它放在“玻璃盒”里。这会阻止你知道里面是一个球吗?你显然可以清楚地看到,不是吗?就像这样... - Sriram Sakthivel
这是一个有点令人困惑的问题。想象一下,你创建了一个 Foo 实例(引用类型),然后将其分配给一个 object 变量。如果你尝试将其转换为 Bar 实例(也是引用类型),难道不也会期望出现相同的异常吗?正如 @JonathonReinhart 所说,装箱/拆箱并没有真正涉及到它。 - Daniel Kelley
@DanielKelley,我承认我没有提到我的困惑,即当装箱/拆箱发挥作用时,即使从一个隐式可转换类型进行转换也不起作用。请看我的编辑。 - Arpit Khandelwal
4个回答

12
当一个值被装箱时,它会获得一个对象头。这个头是从System.Object派生的任何类型都有的。该值跟随该头。头包含两个字段,一个是"syncblk",它具有超出问题范围的各种用途。第二个字段描述对象的类型。
这就是你要问的内容。在文献中有各种名称,最常见的是"type handle"或"method table pointer"。后者是最准确的描述,它是指向CLR加载类型时跟踪信息的指针。许多框架功能都依赖于它。当然,Object.GetType()也是如此。您代码中的任何转换以及"is"和"as"运算符也使用它。这些转换是安全的,因此您不能将Dog转换为Cat,类型句柄提供了这个保证。您装箱的int的方法表指针指向System.Int32的方法表。
在.NET 1.x中,装箱非常普遍,在泛型可用之前。所有常见的集合类型都存储对象而不是T。因此,将元素放入集合需要(隐式)装箱,再次取出则需要显式转换并取消装箱。
为了使其高效,JIT很重要的一点是不需要考虑需要进行转换的可能性。因为那需要更多的工作。因此,C#语言包括了解除装箱到另一种类型是非法的规则。现在只需要检查类型句柄以确保它是预期类型即可。JIT直接将方法表指针与您的情况下System.Int32的指针进行比较。并且嵌入在对象中的值可以直接复制,而无需考虑任何转换问题。非常快,可以使用内联机器代码完成所有操作,而无需任何CLR调用。
这个规则是特定于C#的,VB.NET没有这个规则。这两种语言之间的典型权衡,C#的重点是速度,而VB.NET的重点是方便。在取消装箱时转换为另一种类型不是问题,所有简单的值类型都实现了IConvertible。您可以在代码中显式编写它,使用Convert帮助类:
        int i = 123;                    // A value type
        object box = i;                 // Boxing
        long j = Convert.ToInt64(box);  // Conversion + unboxing

这与 VB.NET 编译器自动生成的代码非常相似。


感谢您的精确解释。Luaan、Paval和Ritesh的答案也从不同的角度解释了同样的事情,但是您的解释恰到好处。 - Arpit Khandelwal

4
这是因为拳击指令将值类型标记添加到结果对象中MSDN。当您从对象中取消装箱值时,该变量是已知类型(以及在内存中的大小)。因此,您必须将对象强制转换为原始值类型。
在您的示例中,您甚至不需要将其从int转换为long,因为这是一个隐式转换。

2
当你使用 boxing 而不是将值类型从堆栈移动到堆中时,它会在堆中创建一个副本,并在新的堆栈盒子中存储它的引用。 因此,你的原始堆栈对象,即值类型对象及其数据类型信息,保留在堆栈中并保持其历史记录。 现在,在拆箱时,它会将堆中对象的类型与堆栈中的原始数据类型进行比较,如果发现不匹配,则会产生错误。 因此,在进行拆箱时必须使用与装箱时相同的数据类型。

1
每个引用对象都有一堆与之关联的元数据。这包括给定对象的确切类型(这就是为什么你可以完全保证类型安全性)。
因此,尽管“int”是按值传递的,但实际上缺少了这些信息(并不重要),但一旦将其装箱,它就会创建一个具有所有必要元数据的新对象。这也意味着,虽然“int”只是4个字节,但装箱的“int”比这多得多——现在你有了一个引用(4-8个字节)、值本身(4个字节)和元数据(其中包括特定类型句柄)。这与C++非常不同,后者允许您将任何指针强制转换为任何类型的指针(并在您强制转换错误时留下处理错误的工作)。

所有按引用传递的对象都有这些元数据。这是引用类型成本相当重要的一部分,但也是确保类型安全的手段。这还很好地展示了 ArrayList of int 可能真正的昂贵程度,以及为什么 int[]List<int> 更加高效——即使忽略了分配(更重要的是收集)堆对象的成本和装箱/拆箱本身,4 字节的 int 可能会突然变成 20 字节,只因为你在存储对它的引用 :)


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接