语言基础之类型基础(1)
一:语言基础类型Object
在语言设计时,要求每个类型都从System.Object类型派生,这点大家都非常清楚,所以每个类型都具备最基础的方法,如Equals/GetHashCode,其中Equals其实应该叫做具有同一性,而这个同一性就是指向当前对象的引用地址,如下所示.
所以当对象是null时,是不会执行equals方法,而是c#语言本身的命令,而如果一个对象不属于null,其中this是指向当前的引用地址
这里就出现一个标准就是c#在重写equals的时候,需要对HashCode进行重写,如果没有,那么会有一个警告.原因是重写equals必须确保相等性算法和对象哈希吗算法一致.
而对于hashcode,程序有自己的一套hash算法,避免hash碰撞,但是仅限于int型,所以在42亿以内(正整数)hash都可以是无碰撞的(不过因为生日悖论,实际小很多),但是一旦数据量比较大的时候,就会出现不同对象具有同一hash值,只是这种基本上不用担心,担心既然有其他方式解决,比如生成自己的double的hash存储.
上面就会出现,当所有具有相同名字的类型名字hashcode都相等,方便在做Dictionary或者是hashmap做存储时候使用.
注意:hashcode不等同于引用地址 所以 == 与equals不一致 与GetHashCode也不一致, ==是引用地址,在64位下,在源码3.12.5的内核代码显示的是HardCode,属于64位无符号位算法,远远大于HashCode的HashCode,所以 == 指向的内存地址,简单说就是 == 包含hashcode,而== 指向的内存地址,除非重写==,否则都是指向不同的位置.
二:如何生成对象
c#要求所有对象都用new操作符创建. EqualsTest test = new EqualsTest() ;
在new对象时候,会经过几个专门的标准步骤,这种步骤不仅仅在C#中是如此,在java/js中也是如此(虽然我想讲在所有的高级语言都是如此).
注意:这里有几个不同的名词:1.类型对象 2.实例对象 3.类型对象指针 4.静态构造器 5.实例构造器
1.计算类型及其所有的基类型中定义的所有的实例字段所需要的字节数.堆上每个对象都需要额外的成员,包括类型对象指针和同步块索引
2.从托管堆中分配类型要求的字节数,从而分配对象的内存,分配的内存全部初始化为零(注意是地址大小为0.如果为对象那么为null bool为false等等).
3.初始化对象的类型对象指针 和同步块索引成员
4,调用类型的实例构造器,传递在new调用中指定的实参.大多数编译器都在构造器中自动生成代码来调用基类构造器.每个类型的构造器都负责初始化该类型定义的实例字段,最终调用Object构造器,该构造器什么都不做,简单的返回.
上面属于构造对象的最基本的步骤方式,这也是编译原理中最基本的几个根节点,注意,也是c#/java中一切皆为对象的根本实现方式,此处的对象是指一切对象包含类型对象和实例对象
接下来我们通过运行时的相互关系来逐步验证上面的实现方式,然后在通过加载顺序来解析正确性.
通过Child与Parent类来解析父类与子类之间的关系,以及类型加载的关系
步骤分析:
1.首先遇到加载Object类型至堆上,包括同步块索引 和类型对象指针及GetHashCode/Equals等公共字段,这里没有显示
2.运行至Main方法内部的Parent child,那么首先加载Parent类型对象至堆中,其实应该更名为 Type对象,里面包含的TypeName指向为Parent,包含PropertyInfo MethodInfo等系统信息,但是这属于反射数据,目前就当做Type类型中的简单字段就好
3.运行至child=new Child();加载Child类型对象进入系统,会加载所有的静态字段及方法名.然后加载new执行的Child实例对象,实例对象的类型对象指针指向类型对象,会将所有的字段都加载至当前对象中
4.child.Test();调用方法,因为Test是属于虚实例方法,而c#在调用需实例方法时,总是去检查调用方法的对象的类型,此处苏日安child为Parent类型,但是其为Child()对象,所以输出Child中的Test方法.
5.进行强制转换,内部有个判断机制
6._child.C_T01():此处为实例方法,那么只检查调用方法的当前的类型,而此处的类型是Child,所以直接找类型对象(Child)中的方法,注意:Java为向后运行机制的出现,导致没有此检查,都会执行向后运行机制.
7.调用静态方法,其实就与实例方法类似,只检查调用方法的当前的类型,如果当前类型没有,对象内部有一个原型链(js中的概念,我直接借鉴过来),指向其父类型的类型对象信息,直接调用
以上便是一个类型加载的根本顺序,当启动一个堆时,先加载Object类型,在加载当前使用的类型对象,此处为2个Type对象,一个TypeName=Child 一个TypeName=Parent.,然后当执行new操作,会执行实例对象加载,在实例对象加载时候,即使继承多个父类型,也不会产生多个父类型对象,而是只产生一个对象,此对象在new时,会将其当前类型及其父类型中所有需要实例化的字段,私有属性都初始化(为0),然后调用构造器赋值.最终调用Object构造器返回对象值
细节梳理及问答:
1.会有多个类型对象吗?
答案:每个类型在同一个堆中只会产生一个类型对象.并且因为静态字段在类型对象上,所以导致静态字段可以共享.也是导致不会每次都会执行方法的对象,而是所有的方法都做了一次缓存,存在于当前的对象上.
2.虚方法,实例方法和静态方法在调用时候,有实际区别没?
答案:在调用时候其实是没有区别的,只不过一个是在编译期提示错误,不允许通过对象来调用静态方法,其次在执行期间,会判断当前方法时属于虚方法或者是实例方法,如果是虚方法,那么看调用的对象具体的实现,如果是实例对象,那么就看发起调用的类型.
注意:每个对象都有一个原型链,指向其继承的父类型对象,如果在自身的类型对象上没找到,那么就去找父类型,这个也是调用方法的执行顺序之间的差别,一个从当前到子类,一个是从子类到父类.,另外静态方法在调用时候,执行的是.call(info, null)操作,即不需要传递当前对象,
3.实例对象和类型对象有直接的继承关系吗?
答案:没有,两个相互独立,或者说每个对象根本上来说都是继承自Object的对象,都包含一个指向当前类型对象的指针(类型对象指向的是它本身).而内部初始化不同,是通过执行方式的不同而构造的,实例对象初始化实例字段,而类型对象初始化静态字段/方法
对象加载的顺序
上面是加载的第一步:在初始化对象时候虽然看到是new一次,但是系统执行了两次对象初始化,一是类型对象,只需初始化一次,初始化静态字段/方法,二是每执行new都会创建一个实例对象,初始化实例字段.
注意:虽然他们属于不同的对象,但是创建的方式都是相同的,都包含类型对象指针,同步块索引,和当前对象包含的字段.
现在来更深入点,在每个对象时候,是怎么初始化成对象的.
先直接说结论:当初始化一个对象的时候,先初始化当前对象需要包含的所有字段属性,初始化为零,然后调用父类型构造方法,执行赋值操作,在执行子类型执行赋值操作.
这点可能和国内很多翻译过来的书有典型的区别,大部分书籍都是说先执行父类型构造器,在执行子类型构造器,在创建对象,这个是错误,因为递归只会从子到父,而不可能从父到子.
造成这个原因,很大的原因是没考虑到执行的统一性.
来梳理细节:
1.类型对象和实例对象的初始化顺序是否相同?
答案:所有高级语言都尊崇一个标准,这个标准不会因为它是类型对象或者是实例对象就发生改变,除非能够找到一个关键字去补充,而不是重新定制一个标准.
而类型对象和实例对象最大的区别是关键字static,在c#中静态构造器和实例构造器,在java中虽然只有一个构造器,但是实际也会分成2部分去执行.
2.既然初始化顺序都一致,那么顺序是什么?
答案:以实例对象为例,当在初始化实例对象的时候,首先会计算当前对象及其父类型中所有需要实例化的字段,注意,此处不会创建多个对象,而是只会创建一个对象,这个对象包含所有需要初始化的字段,然后分配内存,初始化为零(这里零已经重复过很多次,也是和很多书本差别的根本原因).初始化为零的操作,只分配当前需要的最大的内存空间,但是所有的内存空间都是空的,比如Int8,会分配32位空间,但是此空间为都是00000000,即整数是0,其他数值是其默认初始值,这时候在调用其实父类的实例构造器,,对父类中的实例字段进行赋值,然后在执行子类的构造器,对子类的实例字段赋值,返回构造完成的对象地址,创建成功.
可以看出具体步骤,都是相同的加载顺序,只不过加载的内容不一样,而这种不一样的方式是通过static关键字来区分的.
3.java中没有static构造器和实例构造器之分,也是这么区分的吗?
答案:的确,只不过编译器在编译代码时候做了一次区分,而根本是这种的,都是先分空间,在初始化为零(在提下为0,指针为0表示为null)在执行由父类到子类的赋值操作.
总结:这个博客是我的第一篇博客,也是比较重要的但是比较让人忽视的理论基础,基本上c#/java/js所有的加载都是从此处出发的,我觉得Coder得知道自己代码从哪里开始,才知道由哪里结束,一步步去掌控自己的代码,而不是单纯的实现出来.
摘要:如果比较熟悉所讲的内容大概需理清以下几个概念
1.类型对象
2.实例对象
3.对象都包含类型对象信息,类型对象指向自身,而实例对象指向类型对象
4.每次只会产生1个对象,而不会new一个子类而new出了所有的父类,所以在这个实例对象中包含自身及其父类型中所有的字段信息
5.加载顺序都是由子去分配空间,到初始化,到执行由父类到子类的赋值操作
6.对象的hashcode与引用值不一样