.NET基础知识-类型、⽅法与继承
类型Type简述
  .NET中主要的类型就是值类型和引⽤类型,所有类型的基类就是System.Object,也就是说我们使⽤FCL提供的各种类型的、⾃定义的所有类型都最终派⽣⾃System.Object,因此他们也都继承了System.Object提供的基本⽅法。
  System.Object可以说是.NET中的万物之源,如果⾮要较真的话,好像只有接⼝不继承她了。接⼝是⼀个特殊的类型,可以理解为接⼝是普通类型的约束、规范,她不可以实例化。(实际编码中,接⼝可以⽤object表⽰,只是⼀种语法⽀持,此看法不知是否准确,欢迎交流)
  在.NET代码中,我们可以很⽅便的创建各种类型,⼀个简单的数据模型、复杂的聚合对象类型、或是对客观世界实体的抽象。
类(class) 是最基础的 C# 类型(注意:本⽂主要探讨的就是引⽤类型,⽂中所述类型如没注明都为引⽤类型),⽀持继承与多态。⼀个c#类Class主要包含两种基本成员:
状态(字段、常量、属性等)
操作(⽅法、事件、索引器、构造函数等)
  利⽤创建的类型(或者系统提供的),可以很容易的创建对象的实例。使⽤ new 运算符创建,该运算符为新的实例分配内存,调⽤构造函数初始化该实例,并返回对该实例的引⽤,如下⾯的语法形式:
  <;类名>  <;实例名> = new <;类名>([构造函数的参数])
  创建后的实例对象,是⼀个存储在内存上(在线程栈或托管堆上)的⼀个对象,那可以创造实例的类型在内存中⼜是⼀个什么样的存在呢?她就是类型对象(Type Object)。
类型对象(Type Object)
  看看下⾯的代码:
int a = 123;                                                          // 创建int类型实例a
int b = 20;                                                            // 创建int类型实例b
var atype = a.GetType();                                              // 获取对象实例a的类型Type
var btype = b.GetType();                                              // 获取对象实例b的类型Type
Console.WriteLine(System.Object.Equals(atype,btype));                  //输出:True
Console.WriteLine(System.Object.ReferenceEquals(atype, btype));        //输出:True
  任何对象都有⼀个GetType()⽅法(基类System.Object提供的),该⽅法返回⼀个对象的类型,类型上⾯包含了对象内部的详细信息,如字段、属性、⽅法、基类、事件等等(通过反射可以获取)。在上⾯的代码中两个不同的int变量的类型(int.GetType())是同⼀个Type,说明int在内存中有唯⼀⼀个(类似静态的)Systen.Int32类型。
  上⾯获取到的Type对象(Systen.Int32)就是⼀个类型对象,她同其他引⽤类型⼀样,也是⼀个引⽤对象,这个对象中存储了int32类型的所有信息(类型的所有元数据信息)。
  关于类型类型对象(Object Type):
>每⼀个类型(如System.Int32)在内存中都会有⼀个唯⼀的类型对象,通过(int)a.GetType()可以获取该对象;
>类型对象(Object Type)存储在内存中⼀个独⽴的区域,叫加载堆(Load Heap),加载堆是在进程创建的时候创建的,不受GC垃圾回收管制,因此类型对象⼀经创建就不会被释放的,他的⽣命周期从AppDomain创建到结束;
>前问说过,每个引⽤对象都包含两个附加成员:TypeHandle和同步索引块,其中TypeHandle就指向该对象对应的类型对象;
>类型对象的加载由class loader负责,在第⼀次使⽤前加载;
>类型中的静态字段就是存储在这⾥的(加载堆上的类型对象),所以说静态字段是全局的,⽽且不会释放;
  可以参考下⾯的图,第⼀幅图描述了对象在内存中的⼀个关系,第⼆幅图更复杂,更准确、全⾯的描述了内存的结构分布。
⽅法表
  类型对象内部的主要的结构是怎么样的呢?其中最重要的就是⽅法表,包含了是类型内部的所有⽅法⼊⼝,关于具体的细节和原理这⾥不多赘述(太多了,可以参考⽂末给的参考资料),本⽂只是初步介绍⼀下,通过下⾯代码讲解来进⾏理解。
public class A
{
public virtual void Print() { Console.WriteLine("A"); }
}
public class B1 : A
{
public override void Print() { Console.WriteLine("B1"); }
}
public class B2 : A
{
public new void Print() { Console.WriteLine("B2"); }
}
  上⾯的代码中,定义两个简单的类,⼀个基类A,,B1和B2继承⾃A,然后使⽤不同的⽅式改变了⽗类⽅法的⾏为。当定义了b1、b2两个变量后,内存结构⽰意图如下:
B1 b1 = new B1();
B2 b2 = new B2();
  ⽅法表的加载:
⽅法表的加载时⽗类在前⼦类在后的,⾸先加载的是固定的4个来⾃System.Object的虚⽅法:ToString, Equals, GetHashCode, and Finalize;
然后加载⽗类A的虚⽅法;
加载⾃⼰的⽅法;
最后是构造⽅法:静态构造函数.cctor(),对象构造函数.ctor();
  ⽅法表中的⽅法⼊⼝(⽅法表槽)还有很多其他的信息,⽐如会关联⽅法的IL代码以及对应的本地机器码等。其实类型对象本⾝也是⼀个引⽤类型对象,其内部同样也包含两个附件成员:同步索引块和类型对象指针TypeHandel,具体细节、原理有兴趣的可以⾃⼰深⼊了解。
⽅法的调⽤:当执⾏代码b1.Print()时(此处只关注⽅法调⽤,忽略⽅法的继承等因素),通过b1的TypeHandel到对应类型对象,然后到⽅法表槽,然后是对应的IL代码,第⼀次执⾏的时候,JIT编译器需要把IL代码编译为本地机器码,第⼀次执⾏完成后机器码会保留,下⼀次执⾏就不需要JIT编译了。这也是为什么说.NET程序启动需要预热的原因。
NET中的继承本质
  ⽅法表的创建过程是从⽗类到⼦类⾃上⽽下的,这是.NET中继承的很好体现,当发现有覆写⽗类虚⽅法会覆盖同名的⽗⽅法,所有类型的加载都会递归到System.Object类。
继承是可传递的,⼦类是对⽗类的扩展,必须继承⽗类⽅法,同时可以添加新⽅法。
⼦类可以调⽤⽗类⽅法和字段,⽽⽗类不能调⽤⼦类⽅法和字段。
⼦类不光继承⽗类的公有成员,也继承了私有成员,只是不可直接访问。
new关键字在虚⽅法继承中的阻断作⽤,中断某⼀虚⽅法的继承传递。
  因此类型B1、B2的类型对象进⼀步的结构⽰意图如下:
在加载B1类型对象时,当加载override B1.Print(“B1”)时,发现有覆写override的⽅法,会覆盖⽗类的同名虚⽅法Print(“A”),就是下⾯的⽰意图,简单来说就是在B1中Print只有⼀个实现版本;
加载B2类型对象时,new关键字表⽰要隐藏基类的虚⽅法,此时B2中的Print(“B2”)就不是虚⽅法了,她是B2中的新⽅法了,简单来说
writeline特点就是在B2类型对象中Print有2个实现版本;
B1 b1 = new B1();
B2 b2 = new B2();
b1.Print(); b2.Print();      //按预期应该输出 B1、B2
A ab1 = new B1();
A ab2 = new B2();
ab1.Print(); ab2.Print();  //这⾥应该输出什么呢?
  上⾯代码中红⾊⾼亮的两⾏代码,⽤基类(A)和⽤本⾝B1声明到底有什么区别呢?类似这种代码在实际编码中是很常见的,简单的概括⼀下:
⽆论⽤什么做引⽤声明,哪怕是object,等号右边的[ = new 类型()]都是没有区别的,也就说说对象的创建不受影响的,b1和ab1对象在内存结构上是⼀致的;
他们的的差别就在引⽤指针的类型不同,这种不同在编码中智能提⽰就直观的反应出来了,在实际⽅法调⽤上也与引⽤指针类型有直接关系;
综合来说,不同引⽤指针类型对于对象的创建(new操作)不影响;但对于对象的使⽤(如⽅法调⽤)有影响,这⼀点在上⾯代码的执⾏结果中体现出来了!
  上⾯调⽤的IL代码:
  对于虚⽅法的调⽤,在IL中都是使⽤指令callvirt,该指令主要意思就是具体的⽅法在运⾏时动态确定的:
callvirt使⽤虚拟调度,也就是根据引⽤类型的动态类型来调度⽅法,callvirt指令根据引⽤变量指向的对象类型来调⽤⽅法,在运⾏时动态绑定,主要⽤于调⽤虚⽅法。
  不同的类型指针在虚拟⽅法表中有不同的附加信息作为标志来区别其访问的地址区域,称为offset。不同类型的指针只能在其特定地址
区域内进⾏执⾏。编译器在⽅法调⽤时还有⼀个原则:
执⾏就近原则:对于同名字段或者⽅法,编译器是按照其顺序查来引⽤的,也就是⾸先访问离它创建最近的字段或者⽅法。
  因此执⾏以下代码时,引⽤指针类型的offset指向⼦类,如下图,按照就近查执⾏原则,正常输出B1、B2 
B1 b1 = new B1();
B2 b2 = new B2();
b1.Print(); b2.Print();      //按预期应该输出 B1、B2
  ⽽当执⾏以下代码时,引⽤指针类型都为⽗类A,引⽤指针类型的offset指向⽗类,如下图,按照就近查执⾏原则,输出B1、A。 
A ab1 = new B1();
A ab2 = new B2();
ab1.Print(); ab2.Print();  //这⾥应该输出什么呢?

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。