为了阐明从一种类型转换为另一种类型时实际发生的情况,提及一些有关引用类型实例如何存储在CLR中的信息可能会有所帮助。
首先,有值类型(struct
s)。
- 它们存储在堆栈中(嗯,它可能是一个“实现细节”,但恕我直言,我们可以放心地假设事情就是这样),
- 他们不支持继承(没有虚拟方法),
- 值类型的实例仅包含其字段的值。
这意味着 a 中的所有方法和属性struct
基本上都是静态方法,this
结构引用作为参数被隐式传递(同样,有一个或两个例外,例如ToString
,但大多不相关)。
所以,当你这样做时:
struct SomeStruct
{
public int Value;
public void DoSomething()
{
Console.WriteLine(this.Value);
}
}
SomeStruct c; // this is placed on stack
c.DoSomething();
这在逻辑上与拥有一个static
方法并将引用传递给SomeStruct
实例在逻辑上相同(引用部分很重要,因为它允许该方法通过直接写入该堆栈内存区域来改变结构内容,而无需将其装箱):
struct SomeStruct
{
public int Value;
public static void DoSomething(ref SomeStruct instance)
{
Console.WriteLine(instance.Value);
}
}
SomeStruct c; // this is placed on stack
SomeStruct.DoSomething(ref c); // this passes a pointer to the stack and jumps to the method call
如果您调用DoSomething
结构,则不存在可能必须调用的不同(覆盖)方法,并且编译器静态地知道实际函数。
引用类型(class
es)有点复杂。
- 引用类型的实例存储在堆上,并且某个引用类型的所有变量或字段仅保存对堆上对象的引用。将一个变量的值分配给另一个变量以及强制转换,只是简单地复制引用,使实例保持不变。
- 它们支持继承(虚拟方法)
- 引用类型的实例包含其字段的值,以及一些与GC、同步、AppDomain 身份和类型相关的额外行李。
如果一个类方法是非虚拟的,那么它的行为基本上就像一个struct
方法:它在编译时是已知的并且不会改变,所以编译器可以发出一个传递对象引用的直接函数调用,就像它对结构所做的那样。
那么,当你转换为不同的类型时会发生什么?就内存布局而言,没什么。
如果您像提到的那样定义了对象:
public class Base
{
public int a;
}
public class Inh : Base
{
public int b;
}
然后你实例化一个Inh
,然后把它转换成一个Base
:
Inh i1 = new Inh() { a = 2, b = 5 };
Base b2 = i1;
堆内存将包含一个对象实例(例如,地址0x20000000
):
// simplified memory layout of an `Inh` instance
[0x20000000]: Some synchronization stuff
[0x20000004]: Pointer to RTTI (runtime type info) for Inh
[0x20000008]: Int32 field (a = 2)
[0x2000000C]: Int32 field (b = 5)
现在,引用类型的所有变量都指向 RTTI 指针的位置(实际对象的内存区域早 4 个字节开始,但这并不重要)。
两者都i1
包含b2
一个指针(0x20000004
在此示例中),唯一的区别是编译器将允许Base
变量仅引用该内存区域(该a
字段)中的第一个字段,而无法进一步通过实例。
例如,同一个字段位于完全相同的偏移量处,但它也可以访问位于第一个字段之后 4 个字节的Inh
下一个字段(距 RTTI 指针 8 个字节的偏移量)。i1
b
所以如果你写这个:
Console.WriteLine(i1.a);
Console.WriteLine(b2.a);
两种情况下的编译代码都是相同的(简化,没有类型检查,只是寻址):
对于i1
:
一种。获取 i1 ( 0x20000004
)的地址
湾。加上4个字节的偏移量得到a
( 0x20000008
)的地址
C。获取该地址的值 ( 2
)
对于b2
:
一种。获取 b2 ( 0x20000004
)的地址
湾。加上4个字节的偏移量得到a
( 0x20000008
)的地址
C。获取该地址的值 ( 2
)
因此,唯一且唯一的实例Inh
是在内存中,未经修改,并且通过进行强制转换,您只是告诉编译器如何表示在该内存位置找到的数据。与纯 C 相比,如果您尝试强制转换为不在继承层次结构中的对象,C# 将在运行时失败,但纯 C 程序会愉快地返回实例中某个字段的已知固定偏移量处的任何内容。唯一的区别是 C# 检查您所做的是否有意义,但变量的类型仅用于允许在同一个对象实例周围走动。
您甚至可以将其转换为Object
:
Object o1 = i1; // <-- this still points to `0x20000004`
// Hm. Ok, that worked, but now what?
同样,内存实例是未修改的,但是您对 的变量无能为力Object
,除了再次向下转换它。
虚拟方法更有趣,因为它们涉及编译器跳过提到的 RTTI 指针以获取该类型的虚拟方法表(允许类型覆盖基类型的方法)。这再次意味着编译器将简单地为特定方法使用固定偏移量,但派生类型的实际实例将在表中的该位置具有适当的方法实现。