代码改变世界

您真的了解类型转换吗?请止步,求解!

2011-08-24 23:57  空逸云  阅读(4425)  评论(100编辑  收藏

前阵子,一名同事问及类型转换的问题,我也仅仅说出目前自己的了解。但回头想想,其中的确大有学问,以前只看到了表面,其内在的表现如何,苦苦翻书,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);
            }
        }
    }

执行结果如下:

image

在这里我并没有采用虚方法,否则结果都是第一个了。可见,即使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,然后输出实例的类型,结果如下:

image

可以看到,三个实例的实际类型都是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如何创建运行时对象