ValueType.GetType() 如何能够确定结构的类型?
ValueType.GetType() 如何能够确定结构的类型?
对于引用类型,对象的内存布局如下:
| Type Object指针 | | 同步块 | | 实例字段... |
对于值类型,对象的布局似乎是这样的:
| 实例字段... |
对于引用类型,GetType意味着从'Type Object指针'中找到对象。给定引用类型的所有对象都指向同一个类型对象(该对象也具有方法表)。
对于值类型,这个指针是不可用的。那么GetType()是如何工作的呢?
我在谷歌上查找,并找到了这个片段...有点模糊。有人可以详细解释一下吗?
解决方案是,存储值的位置只能存储特定类型的值。这由验证器保证。
ValueType.GetType()方法能够确定结构体的类型是因为C#是一种静态类型的语言,每个变量在编译时都有一个确定的类型。值类型不能被继承,因此值类型的对象不需要携带额外的类型信息(与引用类型不同,每个引用类型对象都有一个对象类型头部,因为变量类型和值/对象类型可能不同)。
考虑下面的代码片段。尽管变量类型是BaseRefType,但它指向一个更专门类型的对象。对于值类型来说,由于禁止继承,变量类型就是对象的类型。
BaseRefType r = new DerivedRefType(); ValueType v = new ValueType();
我的困惑之处在于第一个问题。似乎有一些魔法让编译器/运行时可以根据任意变量知道'变量的类型'。
因此,运行时不知何故知道ob是MyStruct类型的,即使VT对象本身没有类型信息。
由于这一点,我可以调用在MyStruct中定义的方法(以及ValueType中的一些方法),而无需对RefType进行装箱。
虽然我在这个问题的确切细节上不是专家,但我建议你查看IL规范(ECMA-335),特别是"constrained"操作码。我相信这就是允许在不进行装箱的情况下调用虚方法的原因(编译调用GetHashCode()与GetType()的代码)。尽管如此,你关于它是两个指针的想法也是错误的。r的值是对对象的引用,而对象本身有一个对其实际类型的引用。v的值只是变量的值,但编译器和运行时仍然知道确切的类型(由于缺乏值类型继承)。
变量的类型存储在编译器中(存储在用于存储标识符/符号的内部数据结构中)。RefType对象必须携带它们的类型的原因是因为"Base b = new Derived()"。即使变量的类型是Base,对象的类型也是不同的Derived类型。对于值类型来说,情况并非如此,因为继承是被禁止的。ValType v将始终是ValType的对象。不需要在每个对象中携带这些信息。在编译时就已经确定了。
在示例中使用ValueType可能有些混淆,因为实际上ValueType是一个引用类型System.ValueType。
调用ValueType.GetType()方法会将值类型进行装箱。通过将值类型移动到堆上,现在有了一个引用类型,该引用类型具有指向该对象类型的指针。
如果要避免装箱,可以调用GetTypeCode方法,该方法返回一个枚举,指示值类型的类型而不进行装箱。
下面是一个显示装箱过程的示例:
C#代码:
class Program { static void Main() { 34.GetType(); } }
Main()方法的IL代码:
.method private hidebysig static void Main() cil managed { .entrypoint .maxstack 8 L_0000: ldc.i4.s 0x22 L_0002: box int32 L_0007: call instance class [mscorlib]System.Type [mscorlib]System.Object::GetType() L_000c: pop L_000d: ret }
修改类型的字面值如下所示,以展示编译器的工作方式:
class Program { static void Main() { 34L.GetType(); } }
在字面值之后添加"L",告诉编译器我希望将此字面值转换为System.Int64。编译器看到这一点,并在发出"box"指令时,看起来像这样:
.method private hidebysig static void Main() cil managed { .entrypoint .maxstack 8 L_0000: ldc.i4.s 0x22 L_0002: conv.i8 L_0003: box int64 L_0008: call instance class [mscorlib]System.Type [mscorlib]System.Object::GetType() L_000d: pop L_000e: ret }
可以看到,编译器已经做好了确定要发出的正确指令的工作,之后由CLR来执行这些指令。
但是,它是如何知道如何正确设置装箱对象的Type Object指针的呢?所有值类型看起来都像一个字节流。例如,一个包含1个int的structA,后面跟着一个包含2个int的structB,看起来就像3个int。struct变量指向第一个实例字段的开头...那么它如何确定类型信息?
请看我发布的示例-注意"box"指令给出了要装箱的类型(由编译器对隐式装箱的支持提供)。该类型用于设置正确的对象类型指针。
假设int a = 34,现在a只指向内存中的一个整数。没有其他东西。现在,当我装箱a时,CLR如何知道我需要将[int32]作为参数传递给box?我希望我表达清楚了...CLR如何将特定的内存块映射到值类型对象上?
CLR不会执行此操作-编译器会执行。编译器在装箱int时将指令放入为"box int32"。这在编译时始终是已知的,因为值类型不能被继承。CLR只是在堆栈上的对象上处理"box int32"。
CLR之所以知道要传递"int32",是因为编译器发出了相应的指令。编译器足够聪明,知道值类型的类型是"int32",因此发出了"box int32"指令。
你已经回答了标题中的问题-虽然这是一个涉及装箱的具体情况,但我的基本问题更多的是,如果对象实例本身不包含任何类型标识符,那么如何确定值类型的类型和方法表。后来在这个线程中找到了答案。谢谢。
太棒了,这就是我要说的全部。
为什么像int foo; foo.GetType();这样的调用不会在编译时被替换为typeof(int)(因为结构体不能继承)?
Gishu,我非常同意你的观点。我问了同样的一连串问题,但没有得到明确的答案。
有趣的是,这种优化对于可空类型无法工作,因为它们的装箱行为不同。int? a = 0; a.GetType()返回typeof(int),而不是typeof(int?)。int? a = null; a.GetType()会引发运行时错误。如果实现了这样的优化,它必须明确排除Nullable<>。