【深入理解CLR】 五.类型基础

上一篇的结束标志着第一部分:CLR基础的结束。上一篇主要介绍了强命名程序集,安全策略,运行时类型的解析(如何一步步找到程序集并进行加载)等内容。回顾一下第一部分:

  1. CLR的执行模型(JIT编译器如何工作)
  2. 类型如何生成到托管模块以及托管模块如何合并为程序集
  3. 强命名程序集、安全策略和运行时如何解析类型引用

对了,恭喜微软在6月4号喜提GitHub,世界最大的闭源公司收购世界最大的开源社区,哈哈哈,估计接下来社区会大力推进.net core了吧,看来是时候开始搞一搞.net core了,前途无量啊。闲言少叙,书归正传,这部分开始对类型涉及的各个方面进行介绍。,本篇博客从以下几个方面分析类型基础:

  1. 类型如何转换(包括is和as的使用)也就是类型安全问题
  2. 命名空间和程序集之间的关系
  3. 运行时代码如何执行的(JIT编译IL代码的具体操作)

本文的行文主线:万类始祖System.Object—类型转换—命名空间和程序集的关系—-运行时代码如何执行(当你new一个对象的时候你做了什么)

万类始祖System.Object

System.Object提供的方法

System.Object是万类始祖,也就是任何类型都隐式或显示派生自System.Object,所以所有的类也都具备System.Object提供的一组基本方法:

公共方法 说明
Equals 判断两个对象是否具有相等的值
GetHashCode 返回该对象知道的哈希码
ToString 默认返回类型的完整名称(this.GetType.FullName)
GetType 返回从Type派生出的一个类型的实例,指出调用GetType的对象的类型

对GetType做一个解释,我们知道所有对象都是从类型new出的实例,但其实类型本质上也是对象,也就是类型对象,而所有的类型对象都派生自Type。所以说GetType获取到的类型(类型对象)其实是Type的一个实例,也就是当前调用对象的类型。这一点后边会介绍到。

受保护方法 说明
MemberwiseClone 创建一个与当前对象完全相同的实例,并返回该实例引用
Finalize 在垃圾回收器判断对象该被回收后但、未被实际回收前调用

和Java对比,方法大同小异,我在之前的博客中介绍过java里的Object常用方法:

传送门https://blog.csdn.net/sinat_33087001/article/details/73883929

也许是看到这里还没出现,也许是C#没有,比JAVA的少了5个线程相关操作。之后看到再回来补上吧。

new操作的时候发生了什么

TML t = new TML("tml最帅了")

当new一个对象的时候发生了以下事情:

  1. 计算类型及所有基类型(一直到System.Object)定义的所有实例字段需要的字节数、堆上的每个对象所需的额外成员:类型对象指针和同步块索引

  2. 从托管堆中分配类型要求的字节数,从而分配对象的内存(包括类型对象指针和同步块索引),也就是上一步中所要求的总字节数。分配的所有字节都设为0。

  3. 初始化对象的类型对象指针和同步块索引

  4. 调用类型的实例构造器,传递在new调用中指定的实参(”tml最帅了”),每个构造器都隐式调用基类构造器(这点和Java的一样,一般在构造方法第一行执行),层层调用,每层构造器都负责初始化本层类型的实例字段,最终调用到System.Object

new执行完之后返回一个指向新建对象的引用,也就是t。

类型转换

CLR的一个重要特性就是类型安全,运行时,CLR总是知道对象的类型是什么(通过GetType方法,因为它是非虚方法,所以一个类型不能伪装成另一个)CLR允许将对象转为它实际类型或者它的任何基类型 注意不能将实际基对象转为派生类型。这点很重要,接下来我会解释。

显示和隐式类型转换

Object o = new Object();
TML t = new TML("tml最帅了");

切记:不管是显示转换还是隐式转换,父类对象(o)都不能转为子类类型TML,这是因为,假设 TML t = new Object()那么t想要调用自己特有的实例方法的时候(不是从Object继承的方法,是TML自有的)发现自己实际指向的对象是Object,在运行时就会找不到该方法而导致报错,而Object o = new TML("tml最帅了"); o能调用的方法在TML里都能找到。当然这只是我的猜测,应该是符合面向对象原理依据的。

接下来介绍下显式和隐式转换:

Object o  = new TML("tml最帅了");  //隐式转换
TML t = (TML)o;                 //显示转换

要注意的是,CLR在编译期间无法准确获取对象的类型,只有在运行期间才会检查核实对象的类型,所谓:编译看左边,运行看右边
这里写图片描述
从上到下我来讲解一下:

  • 1-7都是对象类型本身或者隐式转换,对象(右边new出来的)转为自己实际类型或者自己基类型。
  • 8-9都是要把对象转为自己的子类型,编译时就可以报错。10只是添加了引用,
  • 11这里需要加强转,确没有加,编译时报错。
  • 12其实可以不加强转,加了也不报错,
  • 13加了强转,没问题。
  • 14-15类型加了强转,所以编译的时候会通过,因为CLR在编译时不核实具体对象,但运行的时候发现对象是自己的基类型,是不安全的类型转换。
  • 16里b2本身是B类型的执行D对象,强转为D类型当然可以,然后对于左边的b6相当于隐式转换,所以编译运行都能通过

is和as类型转换操作符

Object o = new Object();
TML t = new TML("tml最帅了");
Boolean b1 = (o is Object)  /b1为true
Boolean b2 = (o is TML)  /b1为false

is操作符检查对象是否兼容于指定类型,注意,is操作符永远不会抛出异常。

if(t is Object){         //第一次检查:t是否兼容于Object
    Object o = (Object)t;//第二次检查:如果兼容,则可以执行类型转换
}

两次安全检查性能损耗无疑是比较大的(因为每次开销都大,CLR会判断t引用 对象的实际类型,然后遍历其继承层次结构,用每个基类型去核对指定类型Object)。

所以as操作符横空出世:as操作符会返回null或者对同一个对象的非null引用

Object o = t as Object;   //只执行这一次类型检查
if(o!=null){
    o.ToString();  //打印结果:TML太帅了
}

命名空间和程序集

命名空间是对相关类型的逻辑分组,所以你的类的名称完全可以和命名空间无关。注意CLR对命名空间一无所知,它检查的都是全类名,所以我们常使用的using来避免全名书写和CLR无关。

引用方式

全名引用

就是将类的全名写进来
这里写图片描述

using+简写

使用using的时候,C#编译器会自动在类型名称前附加System.Text前缀,检查这样生成的名称是否与现有类型匹配,注意,是盲匹,每个using会和所有类型匹配一遍,因为编译器不知道哪个类型属于该using引用的命名空间。
这里写图片描述

发生冲突

当发生两个命名空间存在同样名字的类型的时候,在使用using+简写的时候就会编译错误:类型存在不明确的引用。这个时候就要全名指定啦。
当然还可以像下面这样指定别名:

using  TMLNewName = beijing.TML;

如果出现极端情况,两个命名空间相同,且类型名相同,以上解决方法也都失去作用,这样可以使用外部别名来解决,这里不详细讨论了。

命名空间和程序集的关系

命名空间和程序集之间没有必然的关系,同一命名空间的类可能来自不同程序集,同一个程序集也可能包含不同命名空间的类。一般来说程序集是类型的集合,完成一个功能,命名空间一般来说也是这个程序集的名字,不过当然可以更改。
一般:项目—-程序集—–命名空间,当然也可以修改。只要找的到类就行。
编译器在给类型指定全名后,会遍历引用的程序集,直到找到对应的类型,然后确定程序集的位置,接下来执行编译。

运行时的相互关系

运行时栈,注意,栈从高位内存地址向低位内存地址构建
1,如下右边栈已经初始化分配给局部变量name的内存。
这里写图片描述
2,接下来执行M2方法,M2方法内部使用参数s标识栈位置,并且添加调用方法结束后的返回地址。
这里写图片描述
3,接下来执行M2内部的代码:
这里写图片描述

return之后,返回到状态1,继续执行M1内部的其它代码。

运行时CLR如何Jit编译

通过如下继承关系代码来举例:
这里写图片描述
在如下代码中执行,检测执行过程:
这里写图片描述
执行流程如下所示:

  1. CLR检测M3方法内代码所涉及的类型:Employee,Int32,Manager,String(通过“joe”得知)
  2. CLR确定定义这些类型的程序集都被加载(通过命名空间找程序集),然后利用程序集中的元数据来构建类型,这个过程在我另一篇博文里介绍过:如何加载和利用程序集来构建类型:

    传送门https://blog.csdn.net/sinat_33087001/article/details/80485418

  3. 依据步骤2构建类型对象,在堆上(这里不展示Stirng和Int32)

这三步相当于初始化的过程,初始化之后:
这里写图片描述

接下来的流程就按照M3方法内部的方法顺序开始执行。

new一个对象

在初始化局部变量之后,CLR会自动将所有局部变量设置为null或者0(这里就是e为null,year为0).接下来按照之前说到的new的执行过程创建对象:
这里写图片描述

静态方法的执行

执行静态方法时,CLR会定位与定义静态方法的类型对应的类型对象,这里也就是(Employee)假设lookup返回一个Manager对象的引用,在堆上又构建了一个Manager,当然假设返回的是Employe对象的引用,就是在堆上创建了一个Manager对象:

这里写图片描述

前一个Manager对象会被垃圾回收机制回收掉。该方法被调用后执行Jit编译

非虚实例方法的执行

执行非虚实例方法的时候,CLR会定位发出调用的那个变量(e)的类型(Employee)对应的类型对象
这里写图片描述

虚实例方法的执行

执行虚实例方法的时候,CLR会首先检查发出调用的变量(e),并跟随地址来到具体发出调用的对象(Manager)。
这里写图片描述

所有类型对象都是System.Type的类型实例

这里写图片描述

GetType方法返回指向对象的类型对象的指针。这也解释了为什么可以通过GetType获取任何对象的类型对象了。

依照惯例唠叨两句,本篇博客主要介绍了类型转换如何进行,is和as操作符有什么区别,命名空间程序集的关系,当然最重要的就是方法执行的时候内存堆栈是如何执行的,在上一篇博客里提到的加程序集生成类型之后,CLR如何对应不同方法进行定位,再次恭喜微软喜提GitHub,这大大加强了我搞.net core的决心。

猜你喜欢

转载自blog.csdn.net/sinat_33087001/article/details/80605090