【深入理解CLR 五】类型基础
上一篇的结束标志着第一部分:CLR基础的结束。上一篇主要介绍了强命名程序集,安全策略,运行时类型的解析(如何一步步找到程序集并进行加载)等内容。回顾一下第一部分:
- CLR的执行模型(JIT编译器如何工作)
- 类型如何生成到托管模块以及托管模块如何合并为程序集
- 强命名程序集、安全策略和运行时如何解析类型引用
对了,恭喜微软在6月4号喜提GitHub,世界最大的闭源公司收购世界最大的开源社区,哈哈哈,估计接下来社区会大力推进.net core了吧,看来是时候开始搞一搞.net core了,前途无量啊。闲言少叙,书归正传,这部分开始对类型涉及的各个方面进行介绍。,本篇博客从以下几个方面分析类型基础:
- 类型如何转换(包括is和as的使用)也就是类型安全问题
- 命名空间和程序集之间的关系
- 运行时代码如何执行的(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一个对象的时候发生了以下事情:
-
计算**类型及所有基类型(一直到System.Object)定义的所有实例字段需要的字节数、**堆上的每个对象所需的额外成员:类型对象指针和同步块索引。
-
从托管堆中分配类型要求的字节数,从而分配对象的内存(包括类型对象指针和同步块索引),也就是上一步中所要求的总字节数。分配的所有字节都设为0。
-
初始化对象的类型对象指针和同步块索引
-
调用类型的实例构造器,传递在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编译
通过如下继承关系代码来举例:
在如下代码中执行,检测执行过程:
执行流程如下所示:
- CLR检测M3方法内代码所涉及的类型:Employee,Int32,Manager,String(通过“joe”得知)
- CLR确定定义这些类型的程序集都被加载(通过命名空间找程序集),然后利用程序集中的元数据来构建类型,这个过程在我另一篇博文里介绍过:如何加载和利用程序集来构建类型:
传送门https://blog.csdn.net/sinat_33087001/article/details/80485418
- 依据步骤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的决心。