《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何时创建类型对象

当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对象

CLR在堆中创建对象的步骤:

(1)       初始化类型对象指针,使之指向对应类型对象

(2)       初始化同步索引块

(3)       初始化实例字段(实例字段包括本身及其基类的实例字段)

(4)       调用类型的构造器(它本质上是可能修改某些实例数据字段的一个方法)。New操作法会返回对象内存地址,该地址保存在栈中的变量e中

 

图9 Employee的静态方法Lookup为Joe分配并初始化一个Manager对象

调用静态方法的步骤(为何如此复杂,参考8.6扩展方法):

(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.");
        }
    }
View Code
        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();
        }
View Code

只有多态方法,没有多态实例字段【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

原文地址:https://www.cnblogs.com/Ming8006/p/2934699.html