前阵子,一名同事问及类型转换的问题,我也仅仅说出目前自己的了解。但回头想想,其中的确大有学问,以前只看到了表面,其内在的表现如何,苦苦翻书,Google几番之后,依然无所收获,故大胆写下,求园中各位大牛不吝解答。
类型转换的疑惑
首先,我们知道类型转换也就那点事(表面的说),总归而言,C#下有几种转换,装箱,拆箱,向上类型转换,向下类型转换,平行类型转换几种。这几种的区别目前也不细说了,感兴趣的童鞋可移步C# 装箱和拆箱[整理],向上类型转换,向下类型转换,平行类型(.Net本质论79页-运行时的类型)。
依照以往的知识,现在我们假设有一个Person类,再有一个Employee类,Employee继承Person,声明一个Employee的实例,并将其赋给一个Person的实例,由于类型是引用类型,则实际上它们都是指向同一个对象实例。代码如下:
Employee kinsen = new Employee("Kinsen", "Chan");
Person kong = kinsen;
这一点应该是毫无疑问的。问题是,已知在构造一个实例的时候,实际上是在堆栈上开辟一块空间,这块空间包含三块,分别是同步块,类型句柄,以及实例具体信息。我们就是通过类型句柄来获得该实例的具体对象。但此时Person类实例kong指向的是kinsen实例的地址,那么该类型句柄的信息也应该是Employee而非Person的。但偏偏我们却能正确的获取到Person的方法,也能正确的执行,看一下代码:
public class Program
{
static void Main(string[] args)
{
Employee kinsen = new Employee("Kinsen", "Chan");
kinsen.SayHello();
Person kong = kinsen;
kong.SayHello();
}
public class Person
{
public string FirstName { get; set; }
public string LastName { get; set; }
public Person(string firstname, string lastName)
{
this.FirstName = firstname;
this.LastName = lastName;
}
public void SayHello()
{
Console.WriteLine("Hello,Word");
}
public override string ToString()
{
return FirstName + " " + LastName;
}
}
public class Employee : Person
{
public Employee(string firstname, string lastname) : base(firstname, lastname) { }
new public void SayHello()
{
Console.WriteLine("Hello,Word!My Name is " + base.FirstName);
}
}
}
执行结果如下:
在这里我并没有采用虚方法,否则结果都是第一个了。可见,即使kong引用的是kinsen,但实际上它执行的还是Person类的方法。那么,到底是从哪里得知kong是Person类对象的呢?再见一个实验。
Employee kinsen = new Employee("Kinsen", "Chan");
kinsen.SayHello();
Person kong = kinsen;
kong.SayHello();
object obj = kong;
Console.WriteLine(kinsen.GetType());
Console.WriteLine(kong.GetType());
Console.WriteLine(obj.GetType());
除了Employee和Person类实例,我们还将kong赋给了一个object,然后输出实例的类型,结果如下:
可以看到,三个实例的实际类型都是Employee,但是Person类实例的确是执行了Person类的SayHello方法啊。这到底是为什么?
到处寻找答案,在《.Net本质论》79页中找到这么一段话:
当从一个对象引用的类型转换到另一个对象引用的类型时,必须考虑两个类型之间的关系。如果初始化引用的类型被认定与新引用的类型兼容,那么,CLR所要做的转换只是一个简单的IA-32 mov指令。这通常出现于这样的赋值情形中;当一个派生类型的引用到一个直接或间接基类的引用,或则到一个一直兼容的接口引用。
从这段话中,了解到为什么结果三个实例的类型都是Employee,但我想解决的问题还没解决,为何指向Employee实例引用的Person类实例还能准确的找到它的类型呢?目前我已知的信息如下:
实例地址,也就是线程栈上的地址,它只包含一个指向堆栈引用的指针。
堆栈内存块,也就是线程栈上保存那个指针指向的地址,它包含三部分,同步快,类型句柄,实例信息。其中类型句柄起到标识该实例所属类型,所拥有方法表等信息,但现状是三个实例指向的都是同一个内存地址,也就是它们是一模一样的。那它们到底是如何识别的?
内存中的表现形式
我再借助SOS来探查具体的信息,稍微改动了下代码,以便在SOS中更好查看。
static void Main(string[] args)
{
new Program().Run();
}
Employee kinsen;
object obj;
Person kong;
public void Run()
{
kinsen = new Employee("Kinsen", "Chan");
kinsen.SayHello();
kong = kinsen;
kong.SayHello();
obj = kong;
Console.WriteLine(kinsen.GetType());
Console.WriteLine(kong.GetType());
Console.WriteLine(obj.GetType());
}
通过!ClrStack命令得到当前对象的地址:
000000000023e7e0 000007ff00190163 DebugTest.ClassConvert.Run()
PARAMETERS:
this = 0x00000000023c5ad0
0:000> !dumpobj 0x00000000023c5ad0
Name: DebugTest.ClassConvert
MethodTable: 000007ff00033b68
EEClass: 000007ff00182250
Size: 40(0x28) bytes
(E:\Projects\ClassConvert.exe)
Fields:
MT Field Offset Type VT Attr Value Name
000007ff00033d68 4000001 8 DebugTest.Employee 0 instance 00000000023c5b48 kinsen
000007fef43773f8 4000002 10 System.Object 0 instance 00000000023c5b48 obj
000007ff00033ca0 4000003 18 DebugTest.Person 0 instance 00000000023c5b48 kong
这里能看到DebugTest.ClassConvert类有三个实例,分别是kinsen,obj和kong,它们的value都相同(00000000023c5b48),这里的确与程序中看到的一模一样,但是注意,它们的MT,也就是方法表却不一样了。再分别把他们解析一下。
//Name=kinsen
0:000> !dumpvc 000007ff00033d68 00000000023c5b48
Name: DebugTest.Employee
MethodTable 000007ff00033d68
EEClass: 000007ff00183498
Size: 32(0x20) bytes
(E:\Projects\ClassConvert.exe)
Fields:
MT Field Offset Type VT Attr Value Name
000007fef4377b08 4000004 0 System.String 0 instance 000007ff00033d68 <FirstName>k__BackingField
000007fef4377b08 4000005 8 System.String 0 instance 00000000023c5af8 <LastName>k__BackingField
//Name=obj
0:000> !dumpvc 000007fef43773f8 00000000023c5b48
Name: System.Object
MethodTable 000007fef43773f8
EEClass: 000007fef3f42200
Size: 24(0x18) bytes
(C:\Windows\assembly\GAC_64\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll)
Fields:
//Name=kong
0:000> !dumpvc 000007ff00033ca0 00000000023c5b48
Name: DebugTest.Person
MethodTable 000007ff00033ca0
EEClass: 000007ff001833f0
Size: 32(0x20) bytes
(E:\Projects\ClassConvert.exe)
Fields:
MT Field Offset Type VT Attr Value Name
000007fef4377b08 4000004 0 System.String 0 instance 000007ff00033d68 <FirstName>k__BackingField
000007fef4377b08 4000005 8 System.String 0 instance 00000000023c5af8 <LastName>k__BackingField
或许到这里,的确能解释为什么即使指向的对象是同一个,但却能在转换成其他类型之后做该类型的操作,但是其中还是如一个黑匣子,我对此依然不明不白。
令人向往的方法表,方法槽表
此外,还有一张图,
这张图出自微软,对于一些概念,我还是比较模糊,例如方法表,方法槽表,SOS中的MT应该是方法表呢?还是方法槽表?从图上看来,方法表的分布比较散,看起来好像没什么规则,这样又如何确定方法槽表,方法表与方法槽表之间的关系又是如何呢?很希望大家能踊跃回答,如果有详细的资料就更好了。
渴望音讯
关于这个类型转换,类型句柄,方法表的问题纠结折腾了我许久,实在没办法了。才大胆发出来,恳求各位前辈,大大能帮小弟解惑。
更多
可能也有童鞋也和我有一样的疑问,以下是一些我查找的知识点来源:
揭示同步块索引(上):从lock开始
揭示同步块索引(中):如何获得对象的HashCode
揭示同步块索引(下):总结
关于CLR内存管理一些深层次的讨论[上篇]
深入探索.NET框架内部了解CLR如何创建运行时对象