《CLR via C#》设计类型.类型基础

前言

设计类型这部分的十章全部细读。

4.类型基础

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

以下两个类型定义完全一致:

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

System.Object的公共方法:

  1. Equals
  2. GetHashCode:如果某个类型的对象作为哈希表集合(比如Dictionary)的键使用,类型应重写该方法。
  3. ToString:默认返回类型的完整名称。但,1.经常重写该方法来返回 包含对象状态表示的String对象,2.也经常出于调试的目的而重写该方法,用于显示对象各字段的值,调用后得到一个字符串。
  4. GetType:得到调用GetType那个对象的类型,返回的Type对象还可以和反射类配合,用于获取与对象的类型有关的元数据信息。

Equals()、GetHashCode()、ToString()都是虚方法,所以能被重写,Equals()、GetHashCode()重写教程ToString()重写教程;GetType是实方法,不能被重写,也意味着一个类型不能被伪装成另一个类型。

System.Object的受保护方法:

  1. MemberwiseClone:浅拷贝,创建新对象的实例,并将新对象的实例字段设与this对象的实例字段完全一致,并返回对新实例的引用。
  2. Finalize:在GC判断对象应作为垃圾被回收后,在对象的内存被实际回收前,会调用这个虚方法。需要在这个时间点上执行清理工作的类型应重写该方法。

CLR要求所有对象都用new创建,new所做的事情:

  1. 计算类型及其所有基类型中 定义的所有实例字段 需要的字节数。托管堆上每个对象都需要额外的成员——“类型对象指针”和“同步索引块”成员。CLR利用这两个成员管理对象;
  2. 在托管堆中分配所要的字节数,也就是为对象分配内存;
  3. 初始化对象的“类型对象指针”和“同步索引块”成员;
  4. 调用类型的实例构造器(也就是构造函数),传递在new调用中指定的实参;
  5. 最后,返回一个指向新对象的引用。

然而,只有new,没有delete,意味着:GC负责自动自动释放对象的内存。

4.2 类型转换

在运行时,CLR知道每一个对象的类型,我们可用“对象名.GetType()”知道对象的类型。

CLR允许将对象转换为它的实际类型(需强制显示转换)和它的任何基类型(安全的隐式转换)。

使用C#的is和as操作符来转型:
1.is检查对象(在is左侧)的类型是否兼容于指定类型(在is右侧),is不会抛出异常,而是返回bool值。is常用的用法:

if (o is Employee)
{
	Employee e = (Employee)o;
	// 接下来就可使用e了
}

2.as也不会抛出异常,异常时,返回null。as常用的用法:Employee e = o as Employee;

类型安全性检测:

// 假设D是B的子类,已知子类能隐式转换为基类,但基类不能转换为子类(强转也不行)。
B b1 = new D(); // OK
B b3 = new Object(); // 编译时报错,基类不能隐式转为子类
B b4 = (B)new Object(); // 运行时报错,基类不能强转为子类

B b2 = new D(); // b2的类型为B,实际类型为D
D d1 = b2; // 编译时报错,b2需要强转为实际类型D
D d2 = (D)b2; // OK

4.3 命名空间和程序集

命名空间对相关的类型进行逻辑分组,开发人员可通过命名空间定位具体类型。比如:using System.IO.FileStream; FileStream fs = new FileStream(...),由于用using提前引入了System.IO.FileStream,所以,编译器就能将FileStream解析为System.IO.FileStream。然而,CLR对命名空间一无所知,因为编译器已经解析好了。
好处:减少打字,增强可读性。

允许两个或多个同名的类型在不同的命名空间,但引用的时候就要避免产生歧义了,最好是显式地告诉编译器要使用哪个类型,也就是写全名。

自定义命名空间很简单,如下:

namespace A
{
    public class B { } // TypeDef: A.B,这是编译器在类型定义元数据表中加的实际类型名称
    namespace C
    {
        public class D { } // TypeDef: A.C.D
    }
}

同一个命名空间中的类型可能在不同的程序集中实现,比如:System.IO.FileStream类型在MSCorLib.dll程序集中实现,而System.IO.FileSystemWatcher类型在System.dll程序集中实现。

4.4 运行时的相互关系

本节将解释类型、对象、线程栈和托管堆在运行时的相互关系,还会解释调用静态方法、实例方法和虚方法的区别。书上的这一节讲得很精彩,有书的可以去看看这一节,这里只做部分要点记录。

线程栈用于存放实参、方法内定义的局部变量;托管堆用于存放实例对象(也就是对象)和类型对象(每个类都对应一个类型对象),引用类型都是从托管堆分配。可以有多个实例对象同时指向一个类型对象,也就是常说的:一个类可以实例化出多个该类的对象。

举例:

在这里插入图片描述
上图Employee类中的GetYearsEmployed()是实例方法,GetProgressReport()是虚方法,Lookup()是静态方法。下图中假设e是Manager的一个实例对象:
在这里插入图片描述
先解释上图中的引用关系:先是new了一个Manager的实例对象,此时线程栈里的e(e是引用类型)就指向上图中的第一个Manager实例对象;接着,Lookup方法查询数据库来查找"joe",假设joe是公司的一名管理,所以,Lookup方法返回一个新的Manager对象,也就在堆上构造了新的Manager实例对象,此时e就指向上图中的第二个Manager实例对象了,此时的第一个Manager实例对象就等待着GC回收了。这两个Manager实例对象的“类型对象指针”都指向它们的类型对象(也就是Manager类型对象)。同理,Manager类型对象里的“类型对象指针”也指向它的类型对象(也就是Type类型对象)。最后,Type类型对象里的“类型对象指针”只好指向自己了。

注意:Employee和Manager类型对象都包含“类型对象指针”成员,这是由于类型对象本质上也是对象。CLR在创建类型对象时,必须初始化这些成员,那初始化成什么?CLR开始在一个进程中运行时,会立刻为MSCorLib.dll中定义System.Type类型创建Type类型对象,Employee和Manager类型对象都是System.Type类型的对象,所以Employee和Manager类型对象的“类型对象指针”都指向Type类型对象。

再解释静态字段,静态字段存放在类型对象里,而不在类型对象的每个实例对象里,也就是静态字段只属于类,而不属于对象!

最后说静态方法、实例方法和虚方法的区别:
1.调用静态方法就是上面代码的第5行,直接用类型来调用。CLR会找到“定义静态方法的类型”对应的类型对象,然后,JIT编译器在类型对象的方法表中查找与被调用方法对应的纪录项,再对该方法进行JIT编译,再调用JIT编译好的CPU指令。
2.调用实例方法就是上面代码的第6行,要用实例对象来调用。JIT编译器会先找到“e的类型(即Employee,因为e是用Employee声明的)”对应的类型对象(即Employee类型对象)。【如果Employee类型没有定义正在被调用的那个方法,JIT编译器就会回溯Employee的基类(直到Object),并在沿途的每个类型中查找该方法,之所以能这样回溯是因为每个类型对象都有一个指向它基类型的字段。】然后,JIT编译器在类型对象的方法表中查找与被调用方法对应的纪录项,再对该方法进行JIT编译,再调用JIT编译好的CPU指令。
3.调用虚方法就是上面代码的第7行,要用实例对象来调用。要先判断e对象的“类型对象指针”指向的类型对象:由于这个e是一个Manager对象,所以e对象的“类型对象指针”指向Manager类型对象,然后,JIT编译器在类型对象的方法表中查找与被调用方法对应的纪录项,再对该方法进行JIT编译,再调用JIT编译好的CPU指令。所以会调用Manager的GetProgressReport实现。

另外,现在我们可以理解GetType返回的是:指定对象的“类型对象指针”,这样就可以判断任何对象(包括类型对象)的真实类型。

猜你喜欢

转载自blog.csdn.net/BillCYJ/article/details/90316980