《CLR via C#》 class及成员基础

class

System.Object

“运行时”所有的类型最终都从System.Object类型派生。

1// 隐式派生自Object
2class A {}
3
4// 显式派生自Object
5class A : System.Object {}

System.Object的public方法

  • Equals:比较函数
  • GetHashCode:对象哈希值
  • ToString:转换成String类型对象
  • GetType:返回对象的类型

System.Object的protected方法

  • MemberwiseClone:创建类型的新实例,并将新对象的实例字段设置与this对象的实例字段完全一致。返回对新实例的引用
  • Finalize:垃圾回收阶段实际清理之前会调用此方法。

new操作符都做了什么工作

1A a = new A("123");

上面的代码中new操作符所作的事情:

  1. 计算类型机器所有基类型(一直到System.Object)中定义的所有实例字段所需要的字节数。并加上每个对象都需要额外开销的类型对象指针(type object pointer)和同步块索引(sync block index)
  2. 从托管堆中分配1步骤中所计算的类型的字节数,从而分配对象的内存,分配的所有字节都设置为零(0)。
  3. 初始化对象的类型对象指针和同步块索引成员。
  4. 调用类型的实例构造器,传递在new调用中指定的实参。编译器会在构造函数(.ctor)中自动生成代码调用基类构造器 。最终都会调用System.Object的构造函数,但它什么都不做,单纯的返回。
  5. 返回指向新建对象的一个引用(或指针)

类型转换

CLR允许将对象转换为它的(实际)类型或者它的任何基类型。然而,将对象转换为派生类时,因可能转换失败,所以要求显示转换。

is 和 as

is操作符用于检查是否兼容于指定类型,如果对象引用null则总是返回false

1Object o = new Object();
2o is Object;    // 返回true
3o is A;         // 返回false

as操作符用于检查是否兼容于指定类型,并进行转换。如果兼容则返回对于类型,不兼容则返回null。(判断null比检查是否兼容于指定类型快)

1A a = o as A;
2if (a != null) {
3    // ...
4}

using操作符的作用

  • using指令指示编译器尝试为类型名称附加不同的前缀,直到找到匹配项
  • 允许为类型或命名空间创建别名。如using obj = System.Object;

运行时的细节

计算机基础的线程栈细节

例如我有如下两个方法M1和M2。观察这两个方法执行时线程栈中内存的分配变化。

 1void M1() {
 2    string name = "Yang";
 3    M2(name);
 4    return;
 5}
 6
 7void M2(string s) {
 8    Int32 length = s.Length();
 9    Int32 tally;
10    return;
11}
  1. 首先我们的程序应该运行在一个Windows进程上。进程可能又会有多个线程。每个线程创建时会分配到1M的栈空间。栈空间用于向方法传递实参,方法内部的定义的局部变量也在栈上。栈从高位内存地址向低位内存地址构建

准备调用M1方法之前的栈

  1. 方法包含 “序幕”(prologue)代码,在方法开始工作之前进行初始化;也包含 “尾声”(epilogue)代码,在方法工作完后对其清理,以便返回调用者。M1方法调用时序幕代码在线程栈上分配局部变量name的内存。

在线程栈上分配M1的局部变量

  1. 开始正式执行M1。M1调用M2,将局部变量name作为实参传递。造成name被压入栈

M1调用M2时,将实参和返回地址压入线程栈

  1. M2开始执行之前序幕代码为其局部变量length和tally分配内存,然后,M2方法内部代码开始执行。最终,M2抵达return语句,使PC(指令指针)设置成栈中的返回地址,M2的栈帧(stack frame)展开(unwind),恢复成上一张图的状态。之后继续执行M1的代码。

在线程栈分配M2的局部变量

CLR调用时的细节

在上面例子的基础上加入class。有了类型的实例堆就要工作了。

 1class A {
 2    public Int32 GetYears() {}
 3    public virtual String GetProgress() {}
 4    public static A Lookup(String name) { return new B(); }
 5}
 6
 7sealed class B : A {
 8    public override String GetProgress() {}
 9}
10
11void M3() {
12    A a;
13    Int32 year;
14    a = new B();
15    a = a.Lookup("Yang");
16    year = a.GetYears();
17    a.GetProgress();
18}

CLR加载到进程中,堆初始化,线程栈已创建,马上调用M3

  1. JIT编译器将M3的IL代码转换成本机CPU指令,会扫描M3内部所有的类型引用(A, Int32, B, String)。CLR会确认其类型的所有程序集都已加载。然后利用程序集的元数据中与这些类型有关的信息,来创建数据结构来表示类型本身。数据结构中会包含两个额外成员:类型对象指针(type object pointer)和同步索引块(sync block index)、还有类型内定义的静态数据字段和方法表(类型定义的每一个方法都有其对应的记录项)

A和B类型对象再M3调用时创建

  1. M3的代码编译之后,就允许线程执行M3的本机代码。然后序幕代码开始为局部变量分配内存并将其初始化为null或0。

在栈上分配M3的局部变量

  1. 正式开始执行M3。构造了一个A对象a。造成在托管堆中创建A类型的一个实例。a对象也有类型对象指针和同步索引块。还有必要的字节来容纳A类型定义的所有实例数据字段。CLR自动初始化内部的“类型对象指针”成员来引用对象对应的类型对象

分配并初始化B对象

  1. 下一行代码调用A的静态方法Lookup。CLR会定位与定义静态方法的类型对应的类型对象。然后,JIT编译器在类型对象的方法表中查找对应记录项,对方法进行JIT编译(如果需要的话),再调用JIT编译好的代码。返回一个A类型的新对象指针赋值给a

B的静态方法Lookup为String分配并初始化B对象

  1. 下一行代码调用A的非虚实例方法GetYears。JIT编译器会找到发出调用的变量(a)的类型(A)对应的类型对象(A类型对象)。如果此类型没有定义调用的方法,JIT则会回溯类层次结构(一直到Object),并在沿途的每个类型中查找该方法

B的非虚实例方法调用

  1. 下一行代码调用A的虚实例方法GetProgress。调用虚实例方法时,JIT编译器要在方法中生成一些额外代码;方法每次调用都会执行这些代码。这些代码首先检查发出调用的变量,并跟随地址来到发出调用的对象

B的虚实例方法调用

  1. A和B类型对象中的“类型对象指针”都指向System.Type类型。所有的类型对象指针都是System.Type类型的“实例”。GetType返回的是指向对象的类型对象的指针

A和B类型对象是System.Type类型的实例

类型相关

类型可见性

CLR术语 C#术语 描述
Private private 成员只能由定义类型或任何嵌套类型中的方法访问
Family protected 成员只能由定义类型、任何嵌套类型或者不管在什么程序集中的派生类型中的方法访问
Family and Assembly (不支持) 成员只能由定义类型、任何嵌套类型或同一程序集中定义的任何派生类型中的方法访问
Assembly internal 成员只能由定义程序集中的方法访问
Family or Assembly protected internal 成员可由任何嵌套类型、任何派生类型(不管在什么程序集)或者定义程序集中的任何方法访问。
Public public 成员可由任何程序集的任何方法访问

提示

CLR要求接口类型的所有成员都为public可访问性。所以C#在接口中禁止显式指定接口成员的可访问性;编译器自动将所有成员的可访问性设为public

提示

C#中派生类重写基类型定义的成员时,要求原始成员和重写成员具有相同的可访问性。在CLR中从基类型派生时,允许放宽但不允许收紧成员的可访问性

提示

友元程序集:A程序集是B程序集的友元程序集,则A程序集可以访问B程序集中的所有internal的方法。

静态类

不需要实例化的类。用static关键字定义,C#编译器会将static解析为abstract和sealed

1// 创建静态类
2static class A {}
3// 编译后为
4abstract sealed class A {}

C#编译器对静态类的限制:

  • 静态类必须从基类System.Object派生。
  • 静态类不能实现任何接口,这是因为只有使用类的实例时,才可调用类的接口方法。
  • 静态类只能定义静态成员。
  • 静态类不能作为字段、方法参数或局部变量使用。

分布类、结构和接口

partial关键字告诉C#编译器:类、结构或接口的定义源码可能要分散到一个或多个源码文件中。

编译时会将partial关键字应用的所有文件编译时合并到一起。在最后生成的文件中生成单个类型。