为了阐明从一种类型转换为另一种类型时实际发生的情况,提及一些有关引用类型实例如何存储在CLR 中的信息可能会有所帮助。
首先有value types (structs)。
- 它们存储在stack 上(嗯,它可能是一个“实现细节”,但恕我直言,我们可以放心地假设事情就是这样),
- 它们不支持继承(无虚拟方法),
- 值类型的实例仅包含其字段的值。
这意味着struct 中的所有方法和属性基本上都是static methods,其中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,则不存在可能必须调用的不同(覆盖)方法,并且编译器静态地知道实际函数。
Reference types (classes) 有点复杂。
如果一个类方法是非虚拟的,那么它的行为基本上就像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 字段),无法进一步了解实例。
对于Inh 实例i1,相同的字段位于完全相同的偏移量处,但它也可以访问位于第一个字段之后 4 个字节的下一个字段b(距RTTI 指针)。
所以如果你这样写:
Console.WriteLine(i1.a);
Console.WriteLine(b2.a);
编译后的代码在两种情况下都是相同的(简化,没有类型检查,只是寻址):
-
对于i1:
一个。获取i1的地址(0x20000004)
b.加上4个字节的偏移量得到a(0x20000008)的地址
c。获取该地址的值 (2)
-
对于b2:
一个。获取b2的地址(0x20000004)
b.加上4字节的偏移量得到a(0x20000008)的地址
c。获取该地址的值 (2)
因此,Inh 的唯一实例在内存中,未修改,并且通过执行转换,您只是告诉编译器如何表示在该内存位置找到的数据。与纯 C 相比,如果您尝试强制转换为不在继承层次结构中的对象,C# 将在运行时失败,但纯 C 程序会愉快地返回位于 known fixed offset 的任何内容您实例中的某个字段。唯一的区别是 C# 会检查您所做的事情是否有意义,但变量的类型仅用于允许在同一个对象实例中走动。
您甚至可以将其转换为 Object:
Object o1 = i1; // <-- this still points to `0x20000004`
// Hm. Ok, that worked, but now what?
同样,内存实例是未修改的,但是您对Object 的变量无能为力,除了再次向下转换它。
Virtual methods 更有趣,因为它们涉及编译器跳过提到的 RTTI 指针以到达该类型的 virtual method table(允许类型覆盖基类型的方法)。这再次意味着编译器将简单地为特定方法使用固定偏移量,但派生类型的实际实例将在表中的该位置具有适当的方法实现。