《CLR via C#》读书笔记 之 类型基础
第四章 类型基础
2013-02-27
4.2 类型转换
4.4 运行时相互关系
例1 展示了在调用方法时,线程栈是如何处理局部变量和参数的
托管堆的内存分配机制
例2 展示了在调用方法时,托管堆是如何工作的
JIT何时创建类型对象
创建对象
调用静态方法
调用非虚实例方法
调用的是虚实例方法
4.2 类型转换
CLR最重要的特性之一就是类型安全性。在运行时,CLR总能知道一个对象时什么类型。由于GetType是非虚方法,所以一个类型不可能伪装成另一个类型,通过这个方法,可以知道对象的确切类型。
CLR允许将一个对象转换为它的(实际)类型或它的任何基类型。
使用C#is和as操作符
is 检查一个对象是否兼容于(实际类型或基类型)指定类型,并返回一个布尔值。
as 检查一个对象是否能强制转换成指定类型,若不可以,返回null
4.4 运行时相互关系
本节将揭示类型、对象、线程栈和托管堆在运行时的相互关系。此外,还将解释调用静态方法、实例方法和虚方法的区别。
例1 展示了在调用方法时,线程栈是如何处理局部变量和参数的
图1展示了已加载了CLR的一个进程。在这个进程中,可能有多个线程。一个线程创建时,会分配到1MB大小的栈。这个栈空间用于向方法传递实参,也用于方法内部的局部变量。在图中,线程已执行了一些代码,准备调用M1方法。
图1一个线程的栈,准备调用M1方法
图2 在线程栈上分配M1方法中的局部变量
图3 M1调用M2时,将实参和返回地址压入栈中
图4 在线程栈中分配M2的局部变量
在线程执行M2内部代码,最终抵达return语句,造成CPU指针被设置成栈中的返回地址,而且M2的栈帧(代表当前线程的调用栈的一个方法调用)会展开(unwind),使线程栈返回到M1调用M2之前的状态,如图2。同理M1调用完后,线程栈会返回到如图1的状态。
托管堆的内存分配机制【1】
引用类型的实例分配于托管堆上,而线程栈却是对象生命周期开始的地方。对32位处理器来说,应用程序完成进程初始化后,CLR将在进程的可用地址空间上分配一块保留的地址空间,它是进程(每个进程可使用4GB)中可用地址空间上的一块内存区域,但并不对应于任何物理内存,这块地址空间即是托管堆。
托管堆又根据存储信息的不同划分为多个区域,其中最重要的是垃圾回收堆(GC Heap)和加载堆(Loader Heap),GC Heap用于存储对象实例,受GC管理;Loader Heap又分为High-Frequency Heap、Low-Frequency Heap和Stub Heap,不同的堆上又存储不同的信息。Loader Heap最重要的信息就是元数据相关的信息,也就是Type对象,每个Type在Loader Heap上体现为一个Method Table(方法表),而Method Table中则记录了存储的元数据信息,例如基类型、静态字段、实现的接口、所有的方法等等(此句与本书下面例2中的类型对象有矛盾,不过本书中的类型对象更易理解)。Loader Heap不受GC控制,其生命周期为从创建到AppDomain卸载。
例2 展示了在调用方法时,托管堆是如何工作的
源代码:
1 internal class Employee 2 { 3 public Int32 GetYearsEmploye() { return -1; } 4 public virtual String GenProcessReport() { return string.Empty; } 5 public static Employee LockUp(String name) { return null; } 6 } 7 internal class Manager : Employee 8 { 9 public override string GenProcessReport() { return base.GenProcessReport(); } 10 }
注意区分堆中的类型对象和对象。
图5 CLR已加载到进程中,它的堆已初始化,一个线程的栈已创建,现在马山要调用M3
图6 Employee和Manager类型对象会在M3被调用创建
当JIT编译器将M3的IL代码转换成本地CPU指令时,会注意到M3内部引用的所有类型:Employee,Int32,Manager以及String(”Joe”)。这个时候,CLR要确保定义了这些类型的所有程序集都已加载。然后,利用程序集的元数据,CLR提取与这些类型有关的信息,并创建一些数据结构来表示类型本身。在图6中,为了一目了然,我们在堆中只显示Emplyee和Manager,Int32和String类型对象很可能在调用M3之前就被创建了。
类型对象包括:
(1)类型对象指针(type object pointer)
(2)同步索引块(sync block index)
(3)静态数据字段
(4)方法表
图7 在线程栈上分配M3的局部变量
图8 分配并初始化一个Manager对象
(1) 初始化类型对象指针,使之指向对应类型对象
(2) 初始化同步索引块
(3) 初始化实例字段(实例字段包括本身及其基类的实例字段)
(4) 调用类型的构造器(它本质上是可能修改某些实例数据字段的一个方法)。New操作法会返回对象内存地址,该地址保存在栈中的变量e中
图9 Employee的静态方法Lookup为Joe分配并初始化一个Manager对象
(1) CLR会定位于定义静态方法的对应的类型对象。
(2) JIT编译器在类型对象的方法表中查找与被调用的方法对应的记录项,对方法进行JIT 编译(如有还没编译的话),在调用之。
假定Employee通过查询数据库查找名家“Joe”的employee,发现Joe是一个经理,所以在内部,Lookup方法在堆上构造了一个新的Manger对象。
注意,e不再引用第一个Manager对象。而且这个对象因没有变量引用,将会变成垃圾回收的主要目标。
图10 Employee的非虚实例方法GetYearsEmployed在调用后返回5
(1) JIT编译器会找到与“发出调用的那个变量e的类型(Employee)”对应的类型对象(Employee类型对象)。
(2) 如果Emplyee类中没有定义正在调用的方法,JIT编译器会回溯类层次结构图(一直回溯到Object),查找调用的方法。之所以可以回溯,是因为每个类型对象都有一个字段引用了它的基本类型,这个信息图中没有显示。
图11 调用Employee的虚实例方法GenProgressReport,最终执行的是Manager重写的这个方法
(1) JIT要在方法中生成额外的代码,方法每次调用时,都会执行这些代码。
(2) 这些代码首先检查发出调用的变量,然后找到它所指的对象。在本例中e指向的是Manager对象。
(3) 然后找到对象指向的类型对象。在本例中Manager对象指向的是Manager类型对象。
(4) 若该类型对象方法表内的对应方法是非虚实例方法,调用之;若没找到对应的非虚实例方法,JIT编译器会回像调用非虚实例方法第二步一样回溯层次结构,直到找到匹配的方法。
注意:若Employee发现的Joe是个Employee,而不是Manager,Lookup会在堆内创建一个Employee对象,他的类型对象指针指向Employee类型对象。这样一来,最终执行的就是Eomployee的GenProgressReport实现,而不是Manager的GenProgressReport实现。
Emplyee和Manager类型对象都包含“类型对象指针“成员。这是由于类型对象本质上也是对象。CLR创建类型对象,必须初始化这些成员。那么如何初始化这些成员?
CLR开始在一个进程中运行时,会立即为MSCorLib.dll中定义的System.Type类型定义一个特殊的类型对象。Employee和Manager类型对象是该类型的“实例”。
当然System.Type类型对象本身也是一个对象,它比较特殊,她的“类型对象指针”指向它自己。
另外,System.Object的GetType方法返回的是对象所指向的类型对象。
class Base { public string name = "Base"; public static void StaticMethod() { Console.WriteLine("Base static method."); } public void NonStaticMethod() { Console.WriteLine("Base non static method."); } public virtual void VirtualMethod() { Console.WriteLine("Base virtual method."); } } class Derived : Base { public new string name = "Derived"; public new void NonStaticMethod() { Console.WriteLine("Derived non static method."); } public override void VirtualMethod() { Console.WriteLine("Derived virtual method."); } }
static void Main(string[] args) { Base.StaticMethod(); //Base static method. Derived.StaticMethod(); //Base static method. Base b=new Base(); Derived d=new Derived(); Base c = new Derived(); b.NonStaticMethod(); //Base non static method. d.NonStaticMethod(); //Derived non static method. c.NonStaticMethod(); //Base non static method. b.VirtualMethod(); //Base virtual method. d.VirtualMethod(); //Derived virtual method. c.VirtualMethod(); //Derived virtual method. Console.WriteLine(b.name);//Base Console.WriteLine(d.name);//Derived Console.WriteLine(c.name);//Base Console.Read(); }
只有多态方法,没有多态实例字段【2】
从上述调用虚方法的得知,多态是如何实现的,即使变量e的类型是基类Employee,但只要它指向的对象是派生类Manager,且有覆盖虚方法的实例方法实现,就会调用Manager中的方法。
但实例字段并不会覆盖,见如下代码:
1 class Employee 2 { 3 public string name = "Employee"; 4 } 5 6 class Manager : Employee 7 { 8 public string name = "Manger"; 9 } 10 11 class Program 12 { 13 static void Main(string[] args) 14 { 15 Employee e = new Manager(); 16 Console.WriteLine(e.name); //显示:Employee 17 } 18 }
上述代码,Employee e = new Manager(); 具体过程参见创建引用类型的实例的过程
- 在使用变量调用方法或字段时,只有调用的是虚实例方法,是根据变量指向的对象,再找指向对象指向的类型对象中找相应的方法(多态方法是这样实现的);
- 调用非虚实例方法或字段时,是直接根据变量的对象类型,找相应的方法或字段。
参考
【1】 [你必须知道的.NET]第十九回:对象创建始末(下) http://www.cnblogs.com/anytao/archive/2007/12/07/must_net_19.html
【2】 [你必须知道的.NET]第十五回:继承本质论 http://www.cnblogs.com/anytao/archive/2007/09/10/must_net_15.html