【问题标题】:How runtime knows the exact type of a boxed value type?运行时如何知道装箱值类型的确切类型?
【发布时间】:2014-07-30 11:11:31
【问题描述】:

我明白什么是拳击。值类型被装箱为对象/引用类型,然后作为对象存储在托管堆上。但我无法通过拆箱。

拆箱会将您的对象/引用类型转换回值类型

int i = 123;          // A value type
object box = i;       // Boxing
int j = (int)box;     // Unboxing

好的。但是如果我尝试将一个值类型拆箱成另一个值类型,例如,在上面的例子中,它会抛出 InvalidCastException

long d = (long)box;

它给我留下了一个想法,可能是运行时隐式知道装在“box”对象内的值类型的实际类型。如果我是对的,我想知道这个类型信息存储在哪里。

编辑:

因为int 可以隐式转换为long。这让我很困惑。

int i = 123;
long lng = i;

非常好,因为它不涉及装箱/拆箱。

【问题讨论】:

  • box.GetType() 怎么样?
  • 运行时知道每个对象的类型。装箱的值也不例外。
  • 假设你有一个值类型的Ball,你在GlassBox中放了一个球。这会阻止你知道它是玻璃盒子里的一个球吗?你可以清楚地看到不是吗?就这样..
  • 这是一个有点令人困惑的问题。假设您创建了一个Foo 实例(引用类型),然后将其分配给object 变量。如果您尝试将其强制转换为 Bar 实例(也是引用类型),您是否也不会期待同样的异常?正如@JonathonReinhart 所说,装箱/拆箱并没有真正涉及。
  • @DanielKelley,我承认我没有提到我的困惑,即当装箱/拆箱发挥作用时,即使从隐式可转换类型转换也不起作用。查看我的编辑。

标签: c# boxing unboxing


【解决方案1】:

当一个值被装箱时,它会得到一个对象头。派生自 System.Object 的任何类型都具有的那种。该值遵循该标题。标头包含两个字段,一个是“syncblk”,它具有超出问题范围的各种用途。第二个字段描述对象的类型。

这就是你要问的那个。它在文献中有各种名称,最常见的是“类型句柄”或“方法表指针”。后者是最准确的描述,它是指向 CLR 在加载类型时跟踪的信息的指针。许多框架功能都依赖于它。 Object.GetType() 当然。您的代码中的任何强制转换以及 isas 运算符都会使用它。这些铸件是安全的,因此您不能将狗变成猫,类型手柄提供了这种保证。盒装 int 的方法表指针指向 System.Int32

的方法表

在泛型可用之前,拳击在 .NET 1.x 中非常很常见。所有常见的集合类型都存储 object 而不是 T。因此,将元素放入集合中需要(隐式)装箱,再次取出它需要使用强制类型转换显式拆箱。

为了提高效率,抖动不需要考虑可能需要转换,这一点非常重要。因为这需要更多的工作。所以 C# 语言包含了拆箱到另一种类型是非法的规则。现在需要的只是检查类型句柄以确保它是预期的类型。在您的情况下,抖动直接将方法表指针与 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 的回答也以不同的方式总体上解释了同一件事,但您的回答恰到好处。
【解决方案2】:

这是因为装箱指令将值类型标记添加到结果对象MSDN。当您从对象中拆箱值时,此变量是已知类型(和内存中的大小)。因此,您必须将对象转换为原始值类型。

在您的示例中,您甚至不需要将其从 int 转换为 long,因为它是隐式转换。

【讨论】:

    【解决方案3】:

    这是因为当您进行装箱而不是将值类型从堆栈移动到堆时,它会在堆中创建它的副本并将它的引用存储在堆栈中的新堆栈框中。 因此,您的原始堆栈对象,即值类型对象及其数据类型信息仍保留在堆栈中并维护其历史记录。 现在在拆箱时,它会将堆中的对象类型与堆栈中的原始数据类型进行比较,如果发现不匹配则给出错误。 因此,有必要使用您在拆箱时装箱的相同数据类型。

    【讨论】:

      【解决方案4】:

      每个引用对象都有一堆与之关联的元数据。这包括给定对象的确切类型(这就是您可以拥有类型安全性的原因)。

      因此,虽然int 是按值传递的,但实际上缺少此信息(并不重要),但是一旦您将其装箱,它就会创建一个包含所有必要元数据的新对象。这也意味着,虽然 int 只有 4 个字节,但装箱的 int 远不止这些 - 你现在有一个引用(4-8 个字节)、值本身(4)和元数据(包括特定类型的句柄)。这与例如非常不同。 C++,它允许您将任何指针转换为任何类型的指针(并在您转换错误时让您处理错误)。

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

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2019-12-22
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多