学习CLR via C#(二) - 类型基础

学习CLR via C#(二)

4.1所有类型都从System.Object派生

CLR要求所有类型最终都要从System.Object派生。也就是所,下面的两个定义是完全相同的,

//隐式派生自System.Object
class Employee {
    .....
}
//显示派生子 System.Object
class Employee : System.Object {
  .....  
}

由于所有类型最终都是从System.Object派生的,所以可以保证每个类型的每个对象都有一组最基本的方法。

System.Object提供了如下所示的公共实例方法。

公共方法说明
Equals(Object)确定指定的对象是否等于当前对象。如果两个对象具有相同值就返回ture.
GetHashCode返回对象的值得一个哈希码。如果某个类型的对象要在哈希表集合中作为key使用,该类型应该重写这个方法。方法应该为不同的对象提供一个良好的分布。
ToString该方法默认返回类型的完整名称(this.GetType().FullName)。
GetType返回从Type派生的一个对象的实例,指出调用GetType的那个对象是什么类型。返回的Type类型可以与反射类配合使用,从而获取与对象的类型相关的元数据信息。非虚方法,防止类重写防止破坏类安全性

System.Object的受保护方法

受保护方法说明
MemberwiseClone这个非虚方法能创建类型的一个新实例,并将对象的实例字段设为与this对象的实例字段完全一致。返回的是对新实例的一个引用
Finalize在垃圾回收器判断对象应该被作为垃圾收集之后,在对象的内存被实际回收之前,会调用这个虚方法。需要在回收之前执行一些清理工作的类型应该重写这个方法。
关于MemberwiseClone

浅复制:在C#中调用 MemberwiseClone() 方法即为浅复制。如果字段是值类型的,则对字段执行逐位复制,如果字段是引用类型的,则复制对象的引用,而不复制对象,因此:原始对象和其副本引用同一个对象!

深复制:如果字段是值类型的,则对字段执行逐位复制,如果字段是引用类型的,则把引用类型的对象指向一个全新的对象!

class Program
    {
        public static void Main()
        {
            //创建P1对象
            Person p1 = new Person();
            p1.Age = 42;
            p1.Name = "Sam";
            p1.IdInfo = new IdInfo("081309207");

            //通过浅复制 得到P2对象
            Person p2 = p1.ShallowCopy();
            //分别输出
            Console.WriteLine("对象P1相关属性如下");
            DisplayValues(p1);
            //p1.Name = "";
            //p1.IdInfo.IdNumber = "XXXXX";
            Console.WriteLine("对象P2相关属性如下");
            DisplayValues(p2);

            Console.WriteLine("======p1和p2浅复制中的引用对象地址相同=====");
            Console.WriteLine(p1.IdInfo==p2.IdInfo);
            p1.IdInfo.IdNumber = "081309211";//修改引用对象中的值
            Console.WriteLine("p1==" + p1.IdInfo.IdNumber);
            Console.WriteLine("p2=="+p2.IdInfo.IdNumber);
            Console.WriteLine(p1.IdInfo == p2.IdInfo);//p2中引用对象自己没有修改,却依旧改变了值且和p1中一致,可验证浅复制只能复制值对象

            //现在测试深复制
            Person p3 = p1.DeepCopy();

            p1.Name = "George";
            p1.Age = 39;
            p1.IdInfo.IdNumber = "081309208";
            Console.WriteLine("对象P1相关属性如下");
            DisplayValues(p1);
            //p1.IdInfo.IdNumber = "CCCCCCC";
            Console.WriteLine("对象P3相关属性如下");//p3中的引用对象里的数值未被更改,所以深复制成功
            DisplayValues(p3);
            Console.Read();
        }

        public static void DisplayValues(Person p)
        {
            Console.WriteLine("      Name: {0:s}, Age: {1:d}", p.Name, p.Age);
            Console.WriteLine("      Value: {0:d}", p.IdInfo.IdNumber);
        }
    }

    public class IdInfo
    {
        public string IdNumber;

        public IdInfo(string IdNumber)
        {
            this.IdNumber = IdNumber;
        }
    }

    public class Person
    {
        public int Age;
        public string Name;
        public IdInfo IdInfo;

        public Person ShallowCopy()
        {
            return (Person)this.MemberwiseClone();
        }

        public Person DeepCopy()
        {
            Person other = (Person)this.MemberwiseClone();
            other.IdInfo = new IdInfo(IdInfo.IdNumber);
            other.Name = String.Copy(Name);
            return other;
        }
    }

深浅复制主要用于当创建一个对象需要消耗过多资源时,可以采取复制的方法提升效率!

大话设计模式的原话是这样滴:当你New一个对象时,每New一次,都需要执行一个构造函数,如果构造函数的执行时间很长,那么多次New对象时会大大拉低程序执行效率,因此:一般在初始化信息不发生变化的前提下,克隆是最好的办法,这既隐藏了对象的创建细节,又大大提升了性能!

当然,如果每个类都要写自己的深复制,这岂不是非常非常麻烦,因此,有一个通用的深复制方法,如下:

	/// <summary>
    /// 通用的深复制方法
    /// </summary>
    /// <typeparam name="T"></typeparam>
    [Serializable]
    public class BaseClone<T>
    {
        public virtual T Clone()
        {
            MemoryStream memoryStream = new MemoryStream();
            BinaryFormatter formatter = new BinaryFormatter();
            formatter.Serialize(memoryStream, this);
            memoryStream.Position = 0;
            return (T)formatter.Deserialize(memoryStream);

        }
    }

相关案例如下(通用的深复制方法使用时必须为相关类及类的引用类型加上可序列化标识:[Serializable]):

class Program 
    {
        public static void Main()
        {
            //创建P1对象
            Person p1 = new Person();
            p1.Age = 42;
            p1.Name = "Sam";
            p1.IdInfo = new IdInfo("081309207");


            //现在测试深复制
            Person p3 = p1.Clone();

            p1.Name = "George";
            p1.Age = 39;
            p1.IdInfo.IdNumber = "081309208";
            Console.WriteLine("对象P1相关属性如下");
            DisplayValues(p1);
            p1.IdInfo.IdNumber = "深复制中,修改了P1的IdInfo属性,即使IdInfo是引用类型,也不会影响P3 (深复制中引用类型原始对象和副本分别指向不同的内存地址)";
            Console.WriteLine("对象P3相关属性如下");
            DisplayValues(p3);
            Console.Read();
        }

        public static void DisplayValues(Person p)
        {
            Console.WriteLine("      Name: {0:s}, Age: {1:d}", p.Name, p.Age);
            Console.WriteLine("      Value: {0:d}", p.IdInfo.IdNumber);
        }

     
    }

   [Serializable]
    public class IdInfo
    {
        public string IdNumber;

        public IdInfo(string IdNumber)
        {
            this.IdNumber = IdNumber;
        }
    }

   [Serializable]
    public class Person : BaseClone<Person>
    {
        public int Age;
        public string Name;
      
        public IdInfo IdInfo;

        public Person ShallowCopy()
        {
            return (Person)this.MemberwiseClone();
        }

        public Person DeepCopy()
        {
            Person other = (Person)this.MemberwiseClone();
            other.IdInfo = new IdInfo(IdInfo.IdNumber);
            other.Name = String.Copy(Name);
            return other;
        }

        public override Person Clone()
        {
            MemoryStream memoryStream = new MemoryStream();
            BinaryFormatter formatter = new BinaryFormatter();
            formatter.Serialize(memoryStream, this);
            memoryStream.Position = 0;
            return (Person)formatter.Deserialize(memoryStream);
        }

    }

    /// <summary>
    /// 通用的深复制方法
    /// </summary>
    /// <typeparam name="T"></typeparam>
    [Serializable]
    public class BaseClone<T>
    {
        public virtual T Clone()
        {
            MemoryStream memoryStream = new MemoryStream();
            BinaryFormatter formatter = new BinaryFormatter();
            formatter.Serialize(memoryStream, this);
            memoryStream.Position = 0;
            return (T)formatter.Deserialize(memoryStream);

        }
    }
new一个对象的过程是什么?

**1)**计算对象所需字节数,包括该类型及其基类型定义的所有实例字段所需的字节数和类型对象指针、同步块索引所需字节数,类型指针和同步块索引是CLR用来管理对象的;

**2)**在托管堆上分配该对象所需内存空间;

**3)**初始化类型对象指针和同步块索引;

**4)**执行构造函数,调用类型的实例构造器,向其传入对new的调用中指定的实参。编译器在构造函数中自动生成一段代码调用基类构造函数,每个类型的构造函数在执行时都会初始化该类型定义的实例字段。最后调用的是System.Object的构造器,该构造器只是简单的返回,不会做其它任何事情。

**5)**返回指向新建对象的一个引用,保存在对象变量中。

CLR要求所有对象都是用new操作符来创建。比如

Employee e = new Employee("ConstructorParam1");

以下是new操作符所做的事情:

1)它计算类型及其所有基类型(一直到System.Object)中定义的所有实例需要的字节数。堆上的每个对象都需要一些额外的开销成员——"类型对象指针(type object pointer)“和"同步块索引”(sync block index)。这些成员由CLR用于管理对象。这些额外成员的字节数会计入对象大小。

注: "类型对象指针(type object pointer)"是指向类方法表的指针;"同步块索引(sync block index)"成员,CLR用该字段进行线程同步控制,某些位还可以用作垃圾回收标 记等等。CLR通过这两个成员管理对象实例。

2)它从托管堆中分配指定类型要求的字节数,从而分配对象的内存,分配的所有字节都设为零(0)。

3)它初始化对象的"类型对象指针"和"同步块索引"成员。

4)调用类型的实例构造器,向其传入对new的调用中指定的任何实参(本例中是"ConstructorParam1")。大多数编译器都在构造器中自动生成代码来调用一个基类的构造器。每个类型的构造器在被调用时,都要负责初始化这个类型定义的实例字段。最后调用的是System.Object的构造器,该构造器只是简单的返回,不会做其它任何事情。

new 执行了所有的操作后,会返回执行新建对象的一个引用。在本例中,这个引用会保存到变量e中,具有Employee类型。

注意:上面提到过"类型对象指针",类型对象不是类型的对象/实例,这两者是有区别的。且没有和new操作对应的delete操作符,没办法显示释放对象分配的内存,CLR采用的是垃圾回收机制,能自动检测到一个对象不在被使用或者访问的时候自动释放对象内存

*类型对象指针和同步块索引
类型对象指针

《CLR via C#》中的原话:

任何时候在堆上创建对象,CLR都自动初始化内部的“类型对象指针”成员来引用 与对象对应的类型对象。

在JIT编译器将IL代码转换成本机CPU指令的时候,利用程序集的元数据,CLR提取与代码中类型有关的信息,创建一些数据结构来表示类型本身。

CLR开始在一个进程中运行时,利用MSCorLib.dll中定义的System.Type类型创建一个特殊的类型对象,代码中的类型对象都是该类型的“实例”,因此,它们的类型对象指针成员会初始化成对的System.Type类型对象的引用

System.Object的GetType方法返回存储在指定对象的“类型对象指针”成员中的地址。也就是说,GetType方法返回指向对象的类型对象的指针。这样就可以判断系统中任何对象(包括类型对象本身)的真实类型。

同步块索引

先看如下图:

在这里插入图片描述

首先,CLR创建类Manager,在内存中分配 类Manager 所占用的空间,当创建Manager的实例M1的时候,M1的类型对象指针就指向Manager。

当用lock来锁定M1的时候,M1的同步块索引就指向一个同步块**(这里说明一下同步块,CLR负责创建同步块,可以把它理解为一个数组,数组中的每一个元素就是一个同步块)。**

M1的同步块索引初始为一个负数,表示M1没有同步,当用lock的时候,CLR负责在同步块数组中寻找空闲的同步块,并把M1的同步块索引被设置为一个整数S,S为找到的同步块在同步块数组中的索引。

当lock结束之后,M1的同步块索引又被重新设置为负数。

在这里插入图片描述

可参考链接:

揭示同步块索引(上):从lock开始 - 横刀天笑 - 博客园

C#学习笔记 类型对象指针和同步块索引

结合CLR Via C# 和网上文章,引用类型对象的同步块索引有这么两个作用:

  1. lock

当一个线程lock的时候(即 Monitor.Enter ),该线程会检查参数中的对象的同步块索引, 是否已经有关联的同步块。若没有, CLR就会在全局的SyncBlock数组里找到一个空闲的项,然后将数组的索引赋值给该对象的同步块索引。若存在,则通过同步块索引获取SyncBlock数组的项。然后, 该线程会设置SyncBlock里的内容,标识出已经有一个线程占用了。当有其他线程想lock时,会检查参数的SyncBlock里的内容,发现已经有线程占用了,其他线程就会等待。lock执行完,占用的线程就会释放SyncBlock,其他线程就可以使用了。

  1. GetHashCode

(要说明下同步块索引在32位机器占用32位,高6位作为控制位,表示参与的操作,后26位的含义随着高6位的不同而不同。)GetHashCode时,先通过 ComputeHashCode 方法得出一个哈希值,然后将这个哈希值与其他几个值进行几次或操作,就得到了一个对象的GetHashCode方法返回给我们使用的值。其中参与或操作的一个值,就是同步块索引的后26位的值。而这时同步块索引的高6位代表的含义中就有表示这后26位值用于参加GetHashCode的含义。

链接:https://www.zhihu.com/question/36842869/answer/148491893

4.2类型转换

CLR最重要特性之一就是类型的安全性。在运行时,CLR始终知道一个对象的类型,可以调用GetType方法,得到对象的类型。

CLR允许将一个对象转换为它的实际类型或者它的任何基类型。

C#不要求使用特殊语法即可将一个对象转换为它的任何基类型,因为向基类型的转换被认为是一种安全的隐式转换。但是,将对象转换为它的某个派生类时,C#要求开发人员只能进行显示转换,因为这样的转换在运行时可能失败。

public static void Main() {
   // 不需要转型
   Object o = new Employee();
 
   // 需要进行强制类型转换
   Employee e = (Employee) o;
}

使用C#的is和as操作符来转型

在C#语言中进行类型转换的另一种方式是使用is操作符。is操作符检查一个对象是否兼容指定的类型,并返回一个Boolean值(true和false)。注意,is操作符是不会返回异常信息的。

is操作符通常这样使用:

if ( o is Employe ){
     Employee e = (Employee) o;
}

在这段代码中,CLR实际是会检查两次对象的类型。is操作符首先核实o是否兼容Employee类型。如果是,在if内部,CLR还会再次核实o是否引用一个Employee。CLR的类型检查增强的安全性,但无疑也会对性能造成一定影响。

C#专门提供了 as 操作符,目的就是简化这种代码的写法,同时提升性能。

as操作符通常这样使用:

Employee e = o as Employee;
if ( e != null ){
    //在if中使用e
}

as操作符的工作方式与强制类型转换一样,只是它是不会抛出异常的,如果不能转化,结果就是null。所以,正确的做法就是检查最终生成的引用是否为null。如果企图直接使用转换后的引用,就会抛出异常。

实例考察

下面来看一个类型转换的例子,看每行代码在编译和运行时是否能够正确通过?如果你都答对了,那么本节课的理解就及格了。

还是上面的两个类,Animal和Dog,主要考察类型转换的知识。

代码正确编译错误运行错误
class Program
{
static void Main(string[] args)
{
Object o1 = new Object();
Object o2 = new Animal();
Object o3 = new Dog();
Object o4 = o3;
Animal Animal1 = new Animal();
Animal Animal2 = new Dog();
Dog Dog1 = new Dog();
Animal Animal3 = new Object();√①
Dog Dog2 = new Object();√②
Animal Animal4 = Dog1;
Dog Dog3 = Animal2;√③
Dog Dog4 = (Dog)Dog1;
Dog Dog5 = (Dog)Animal2;
Dog Dog6 = (Dog)Animal1;√④
Animal Animal5 = (Animal)o1;√⑤
Animal Animal6 = (Dog)Animal2;
}
}

错误点解析:

① Animal Animal3 = new Object();

基类向派生类转换应该显示进行,所以编译就报错了。实际上就算改成显示转型也会发生运行时错误,因为对象类型不兼容。可以尝试改成Animal Animal3 = (Animal)o2;

② Dog Dog2 = new Object();

理由同①,正解为Dog Dog2 =(Dog)o3;

③ Dog Dog3 = Animal2;

理由同①,正解为Dog Dog3 =(Dog) Animal2;

④ Dog Dog6 = (Dog)Animal1

基类向派生类显示转型,语法上没有错误因此编译通过。但在运行时,CLR会检查转型操作以确保是将对象转化为它的实际类型或者它的基类型。而Animal1对象是Animal类型而非Dog类型,因此转型时发生失败。如果添加一句Animal1=new Dog();再执行该转型则会成功。

⑤ Animal Animal5 = (Animal)o1;

理由同④。这里不再赘述了。

4.3命名空间和程序集

**命名空间(namespace)**用于对相关的类型进行逻辑分组,开发人员使用命名空间来方便的定位一个类型。对于编译器而言,命名空间的作用是使类型名称变得更长更具唯一性,但CLR并不知道命名空间,访问一个类型时,CLR需要知道该类型的全名以及它所在程序集。

命名空间和程序集不一定是相关的,也就是说它们之间没有必然联系。

using指令为命名空间或类型创建别名

using MySystemText = System.Text.StringBuilder;

namespace MyTest
{
    class Program
    {
        static void Main(string[] args)
        {
            MySystemText myText = new MySystemText();
        }
    }
}

用于解决引用不同命名空间时,两个命名空间中有相同名称但功能不同的类型的问题。

在这里插入图片描述

4.4运行时的相互关系

现在将解释类型、对象、线程栈和托管堆在运行时的相互联系。此外,还将解释调用静态方法、实例方法和虚方法的区别。

我们先从线程栈开始。

1. 图4-2展示了已加载了CLR的一个Windows进程。在这个进程中,可能存在多个线程。一个线程创建时,会分配到一个1MB大小的栈。这个栈的空间用于向方法传递实参,并用于方法内部定义的局部变量。图4-2展示了一个线程的栈内存(右侧)。栈是从高地址向低地址构建的。在图中,线程已执行了一些代码,现在,假定线程开始执行的代码要调用M1方法了。
在这里插入图片描述
2. 在一个最基本的方法中,会有一些"序幕"(prologue)代码,负责在方法开始时做它工作之前对其进行初始化。另外,还包括了"尾声"(epilogue)代码,负责在方法完成工作之后对其进行清理,然后才返回至调用者。M1方法开始执行时,它的"序幕"代码就会在线程栈上分配局部变量name的内存,如图4-3所示。
在这里插入图片描述
3. 然后,M1调用M2的方法,将局部变量name作为一个实参来传递。这造成name局部变量中的地址被压入栈(参见图4-4)。在M2方法内部,将使用名为s的参数变量来标识栈位置(有的CPU架构会通过寄存器来传递实参,以提高性能)。另外,调用一个方法时,还会将一个"返回地址"压入栈中。被调用的方法在结束后,应该返回到这个位置(同样参见图4-4)。
在这里插入图片描述
4. M2的方法开始执行时,它的"序幕"代码就是在线程栈中为局部变量length和tally分配内存。如图4-5所示。
在这里插入图片描述
5. 然后,M2方法内部的代码开始执行。最后,M2抵达它的return语句,造成CPU的指令指针被设置成栈中的返回地址,而且M2的栈帧会展开(出栈),使之看起来类似于图4-3。之后,M1将继续执行在M2调用之后的代码,M1的栈帧将准确反映M1需要的状态。
在这里插入图片描述
6. 最后,M1会返回到它的调用者。同样的是通过CPU的指令指针设置成返回地址来实现的(这个返回地址在图中未显示,但它应该刚好在栈中的name实参上方),而且M1的栈帧会展开(出栈),使之看起来类似于图4-2。之后,调用了M1的方法会继续执行在M1之后的代码,那个方法的栈帧将准确反映它需要的状态。
在这里插入图片描述
CLR运作关系

  1. 假定现在有以下两个类的定义:

    internal class Employee {
        public               int32         GetYearsEmployed()       { ... }
        public    virtual    String        GenProgressReport()      { ... }
        public    static     Employee      Lookup(String name)      { ... }    
    }
    internal sealed class Manager : Employee { 
        public    override   String         GenProgressReport()    { ... }
    }    
    
  2. 我们的Windows进程已启动,CLR已加载到其中,托管堆已初始化,而且已创建一个线程(连同它的1MB的栈空间)。该线程已执行了一些代码,现在马上就要调用M3的方法。图4-6展示了目前的状况。M3方法包含的代码演示了CLR是如何工作的。
    在这里插入图片描述

  3. 当JIT编译器将M3的IL代码转换成为本地CPU指令时,会注意到M3的内部引用的所有类型:Employee、Int32、Manager以及String(因为"Joe")。这个时候,CLR要确保定义了这些类型的所有程序集都已经加载。然后,利用这些程序集的元数据,CLR提取与这些类型有关的信息,并创建一些数据结构表示类型本身。图4-7展示了为Employee和Manager类型对象使用的数据结构。由于这个线程在调用M3之前已经执行了一些代码,所有不妨假定Int32和String类型对象已经创建好了,所以图中没有显示它们。

  4. 先前提过,堆上的所有对象上都包含两个额外的成员:“类型对象指针"和"同步块索引”。如图4-7所示,Employee和Manager类型对象都有这两个成员。定义一个类型时,可以在类型的内部定义静态数据字段。为这些静态字段数据提供支持的字节是在类型对象自身中分配到的。在每个类型对象中,都包含一个方法表。在方法表中,类型中定义的每个方法都有一个对应的记录项。由于Employee有3个方法就有3个记录项,Manager只有一个方法,也就只有一个记录项。
    在这里插入图片描述

  5. 现在,当CLR确定方法需要的所有类型对象都已经创建了,而且M3的代码也已经编译好了,就允许线程开始执行M3的本地代码。M3的"序幕"代码执行时,必须在线程栈中为局部变量分配内存,如4-8所示。作为方法的"序幕"代码的一部分,CLR会自定将所有局部变量初始化为null或零(0)。
    在这里插入图片描述

  6. 然后,M3执行它的代码来构造一个Manager对象。这就会在托管堆中创建Manager类型的一个实例(也就是Manager对象)。如4-9所示。和所有对象一样,Manager对象也有一个"类型对象指针"和"同步块索引"。该对象还包含必要的字节来容纳Manager类型定义的所有实例数据字段,以及容纳由Manager的任何基类(Employee和Object)定义的所有实例字段。任何时候在堆上新建一个对象,CLR都会自动初始化内部"类型对象指针",让它引用(或指向)与对象对应的类型对象(本例就是Manager类型对象)。此外,CLR会先初始化"同步块索引",并将对象的所有实例字段设为nll或为零(0),在调用类型的构造器(它本质上是可能修改某些实例字段的一个方法)。new操作符会返回Manager对象的内存地址,该内存地址保存在变量e中(e在线程栈上)。
    在这里插入图片描述

  7. M3的下一行代码调用Employee的静态方法Lookup。调用一个静态方法时,CLR会定位到与定义静态方法的类型对应的类型对象。然后,JIT编译器在类型对象的方法表中查找被调用的方法对应的记录项,对该方法进行JIT编译(如果需要的话),再调用JIT编译后的代码。就本例,假定Enployee的Lookup方法要查询数据中的Joe。另外,假定数据库中指出Joe是为Manager,所以在内部,Lookup方法在堆上构造一个新的Manager对象,用Joe的信息初始化它,然后返回该对象的地址。这个地址保存在局部变量e中。如图4-10所示。值得注意的是,e不再引用第一个Manager对象。事实上,由于没有变量引用第一个Manager对象,所以它是将来进行垃圾回收时的主要目标。
    在这里插入图片描述

  8. M3的下一行调用Employee的非虚实例方法GetYearsEmployed。调用一个非虚实例方法时,JIT编译器会找到与"发出调用的那个变量(e)的类型(Emplyee)"对应的类型对象(Employee类型对象)。在本例中,变量e被定义成为一个Employee。如果Employee类型没有定义这个方法,JIT编译器会回溯类层次结构(一直到Object),并在沿途的每个类型中查找该方法。之所以能这样回溯,是因为每个类型对象都有一个字段引用了它的基类型,但在图中没有显示。然后,JIT编译器在类型对象的方法表中查找引用了被调用方法的记录项,对方法进行JIT编译(如果需要的话),再调用JIT编译后的调用。在本例中,假定Employee的GetYearsEmployed方法返回5,。这个整数就保存在局部变量year中。如图4-11所示。在这里插入图片描述

  9. M3的下一行代码调用Empolyee的虚实例方法GenProgressReport。调用一个虚实例方法时,JIT编译器要在方法中生成一些额外代码;方法每次调用时,都会执行这些代码。这些代码首先检查发出调用的变量,然后跟随地址来到发出调用的对象。在本例中,变量e引用的是代表"Joe"的一个Manager对象。然后,代码检查对象内出的"类型对象指针"成员,这个成员指向对象的实际类型。然后,代码在类型对象的方法表中查找引用了被调用方法的记录项,对方法进行JIT编译(如果需要的话),再调用JIT编译后的代码。在本例中,由于e引用了一个Manager对象,所以会调用Manager的GenProgressReport实现。如图4-12所示。
    在这里插入图片描述

  10. 注意,在Employee和Manager类型对象都包含"类型对象指针"成员。这是由于类型对象本质也是对象。CLR创建类型对象时,必须初始化这些成员。初始化成什么呢?CLR开始在一个进程中运行时,会立即为MSCOrLib.dll中定义的System.Type类型创建一个特殊的类型对象。Employee和Manager类型对象都是该类型的"实例".因此,它们的类型对象指针成员会初始化成对System.Type类型对象的引用。如图4-13。
    在这里插入图片描述
    当然,System.Type类型对象本身也是一个对象,内部也有一个"类型对象指针"成员。那么这个指针指向的是什么呢?它指向它本身,因为System.Type类型对象本身就是一个类型对象的"实例"。
      现在,我们总算理解了CLR的整个类型系统及其工作方式。System.Object的GetType方法返回的是存储在指定对象的"类型对象指针"成员中的地址。也就是说,GetType方法返回的是指向对象的类型对象的一个指针。这样一来,就可以判断系统中任何对象(包括类型对象本身)的真实类型。

源代码,IL和JIT编译器之间的关系

1)prologue:序幕 与 epilogue:尾声

序幕代码执行时必须在线程栈中为局部变量分配内存。CLR会自动将所有的局部变量初始化为null或0.

尾声代码,负责在方法完成工作之后对其进行清理

2)托管堆

堆上的所有对象都包含两个额外的成员:类型对象指针(type object pointer)同步块索引(sync block index)
在每个类型对象中都包含一个方法表,在方法表中,类型定义的每个方法都对应一个记录项。

任何时候在堆上新建一个对象,CLR都会自动初始化内部类型对象指针成员,让它引用与对象对应的类型对象。此外,CLR会先初始化同步块索引,并将对象的所有实例字段设为null或0。再调用类型的构造器。new操作符会返回对象的内存地址。

3)调用一个静态方法时,CLR会定位与定义静态方法的类型对应的类型对象,然后JIT编译器在类型对象的方法表中查找与被调用的方法对应的记录项,对方法进行JIT编译,再调用JIT编译的代码。

4)调用一个非需实例方法时,JIT编译器会找到与发出调用的那个变量的类型对应的类型对象,然后JIT编译器在类型对象的方法表中查找引用了被调用方法的记录项,对方法进行JIT编译,然后再调用JIT编译后的代码。

5)调用一个虚实例方法时,JIT编译器要在方法中生成一个额外的代码,方法每次调用时,都会执行这些代码。这些代码首先检查发出调用的变量,然后跟随地址来到发出调用的对象。然后代码检查对象内部的“类型对象指针”成员,这个成员指向对象的实际类型。然后,代码在类型对象的方法表中查找引用的被调用方法的记录项,对方法进行JIT编译(如果需要的话),再调用JIT编译过的代码。

6)System.Object的GetType()方法返回的是存储在指定对象的“类型对象指针”成员中的地址。换言之,GetType()方法返回的是指向类型对象的一个指针。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值