CLR via C# - 设计类型

Posted by     "Jordon Li" on Tuesday, April 28, 2020

TOC

CLR via C# - 设计类型

类型基础

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

CLR要求每个类型最终都从System.Object类型派生。

System.Object的公共(public)方法:

public方法 说明
Equals 1. 对象具有相同的值,返回true
GetHashCode 1. 返回对象的值的hash码。 2. 类型的对象在hash表集合中作为键使用,需要重写此方法,方法应该为不同对象提供良好分布。3. 将该方法设计到Object并不恰当,本应在接口中定义。
ToString 1. 默认返回类型的完整名称(this.GetType().FullName)。
GetType 1. 返回从Type派生的一个类型的实例,指出调用此方法的对象是什么类型。2. GetType是非虚方法,以防类重写该方法,隐瞒其类型,进而破坏类型安全性。

System.Object的受保护(protected)方法:

protected方法 说明
MemberwiseClone 1. 非虚方法。 2. 创建类型实例,并将新对象的实例字段和this对象的实例对象设置为完全一样。3. 返回新实力的引用。
Finalize 1. 虚方法。2. 在对象内存被实际回收之前调用。

实例:

  • 类的实例:对象。
  • 类中定义的实例字段/实例成员:类的非静态字段。静态字段属于类,实例成员属于类的对象。

new操作符运行过程:

  1. 计算所需要的字节数:
    • 类型及其所有基类的实例字段所需要的字节数
    • 成员管理对象:类型对象指针(type object pointer)同步块索引(sync block index)所需要的字节数
  2. 从托管堆中分配内存空间,分配的所有字节都设为0。
  3. 初始化对象的类型对象指针同步块索引成员。
  4. 调用类型的构造器,传递指定的实参。自动调用基类构造器,构造器负责初始化类型的实例字段。
  5. 返回指向新建对象的一个引用(指针)。

2. 类型转换

CLR最重要的特性之一是类型安全

  • 隐式转换
  • 显式转换

C#is操作符:检查对象是否兼容于指定类型,返回truefalse

if(o is Employee)	//第一次检查,核实o是否兼容于Employee
{
	Employee e = (Employee)o;	//第二次检查,再次核实o是否引用一个Employee
}

C#as操作符:检查对象是否兼容于指定类型,返回对象的非null引用null

Employee e = o as Employee;
if(e != null)
{
    //...
}

3. 命名空间和程序集

命名空间(namespace):对相关的类型进行逻辑分组,开发人员可通过命名空间方便定位类型。

C#using指令:

  • 引入命名空间
  • 为类型或命名空间创建别名

4. 运行时的相互关系

  • 类型、对象、线程栈、托管堆在运行时的相互关系?
  • 调用静态方法、实例方法、虚方法的区别?

线程栈:一个进程可能有多个线程,创建线程时会分配到1MB的栈。栈空间用于向方法传递实参,方法内部定义的局部变量也在栈上。栈从高位内存地址向低位内存地址构建。

方法:包含序幕代码尾声代码

  • 序幕(prologue)代码:在方法开始工作前对其进行初始化。
  • 尾声(epilogue)代码:在方法完成工作和对其进行清理,以便返回至调用者。

方法调用:

  1. 将实参地址压入栈。
  2. 将方法返回地址压入栈。
  3. 序幕代码为局部分配内存空间。
  4. 执行代码。
  5. return 将CPU的指令指针设置成栈中的返回地址。
  6. 方法的栈帧展开(unwind)。栈帧:当前线程的调用栈中的一个方法调用。

围绕CLR方法调用:

  1. JIT将IL代码转换为CPU指令时,CLR会确认引用的所有类型的程序集已经加载,利用程序集的元数据,提取与类型有关的信息,在托管堆中创建类数据结构(类型对象)来表示类型本身。
  • 类型对象:类型对象指针(type object pointer)+同步块索引(sync block index)+静态字段+方法表…
  • 静态数据字段分配在类型对象自身中。
  1. CLR确认方法所需的所有类型对象都已创建,代码已经编译完成后,执行本机代码。

  2. 序幕代码在线程栈中为局部变量分配内存,CLR自动为局部变量初始化。

  3. 在托管堆中创建类型的实例对象:类型对象指针+同步块索引+(本身和基类的)实例数据字段。

    • CLR自动初始化类型对象指针指向对象对应的类数据结构(类型对象)

    • 在调用类型构造器之前,CLR初始化同步块索引,并初始化对象的实例字段。

    • new操作符返回对象的内存地址,保存到线程栈上的变量中。

  4. 调用静态方法:

    • CLR定位与定义静态方法的类型对应的类型对象
    • JIT编译器在类型对象的方法表中查找与被调用方法对应的记录项,对方法进行JIT编译,调用编译后的代码。
  5. 调用非虚实例方法:

    • JIT编译器找到调用的变量的类型对应的类型对象,查找或回溯查找被调用方法的记录项
    • JIT编译器在类型对象的方法表中查找与被调用方法对应的记录项,对方法进行JIT编译,调用编译后的代码。
  6. 调用实例方法:

    • JIT编译器在方法中生成一些额外代码,每次调用都会执行这些代码。
    • 首先检查调用的变量,并根据地址找到调用的对象。
    • 检查对象内部的类型对象指针,该成员指向对象的实际类型。
    • JIT编译器在类型对象的方法表中查找与被调用方法对应的记录项,对方法进行JIT编译,调用编译后的代码。

基元类型、引用类型和值类型

1. 编程语言的基元类型

基元类型(primitive type):编译器直接支持的数据类型。基元类型直接映射到Framework类库(FCL)中存在的类型。

C#基元类型与对应的FCL类型:

C#基元类型 FCL类型 符合CLS(Y/N) 说明
sbyte System.SByte N 有符号的8位值
byte System.Byte Y 无符号的8位值
short System.Int16 Y 有符号的16位值
ushort System.UInt16 N 无符号的16位值
int System.Int32 Y 有符号的32位值
uint System.UInt32 N 无符号的32位值
long System.Int64 Y 有符号的64位值
ulong System.UInt64 N 无符号的64位值
char System.Char Y 16位Unicode字符
float System.Single Y IEEE32为浮点值
double SystemDouble Y IEEE64为浮点值
bool System.Boolean Y true/false值
decimal System.Decimal Y 128位高精度浮点值,1位是符号,96位是值本身(N),8位是比例因子(k),其余位未使用。(+/-)N*(10的k次方),-28 <= k <= 0
string System.String Y 字符数组
object System.Object Y 所有类型的基类型
dynamic System.Object Y 对于CLR,dynamic和object完全一致。C#编译器允许使用简单语法让dynamic变量参与动态调度。

基本类型可被写成字面值(literal),字面值可被看成类型本身的实例,调用实例方法。

checkedunchecked基元类型操作:

  • 对基元类型执行算术运算时可能造成溢出。
  • CLR提供一些特殊IL指令,允许编译器选择对溢出的处理行为。
    • add/add.ovf, sub/sub.ovf, mul/mul.ovf, conv/conv.ovf
  • C#编译器溢出检查默认是关闭的。
    • 全局打开:/checked+编译器开关
    • 特定区域检查:checkedunchecked操作符

2. 引用类型和值类型

CLR支持两种类型:引用类型值类型

引用类型:类(class)

  • 内存必须从堆上分配。
  • 堆上分配的每个对象都有一些额外的成员(类型对象指针和同步块索引),这些成员必须初始化。
  • 对象中的其他字节总是设为零。
  • 从托管堆上分配对象时,可能强制执行一次垃圾回收。

值类型:结构(struct)/枚举(enum)

  • 轻量级类型。
  • 一般在线程栈上分配。
  • 变量包含本身实例字段,变量中不包含指向实例的指针。
  • 不受垃圾回收机制的控制。

结构(struct):派生自抽象类System.ValueType, System.ValueType派生自System.Object

枚举(enum):派生自抽象类System.Enum, System.Enum派生自System.ValueType

将类型声明成值类型的条件:

  • 必须:
    • 类型具有基元类型的行为。没有成员会修改类型的任何实例字段。不可变(Immutable)类型:没有提供会更改其字段的成员的类型。
    • 类型不需要从其他任何类型继承。
    • 类型也不派生出其他任何类型。
  • 满足任意条件:
    • 类型实例较小(<=16字节)。
    • 类型实例较大,但是不作为方法的实参传递,也不从方法返回。
      • 实参默认以传值方式传递,会对值类型中的字段进行复制,损害性能。
      • 方法返回一个值类型时,实例中的字段会复制到调用者分配的内存中,损害性能。

值类型和引用类型的区别:

  • 对象表示形式:
    • 值类型:未装箱和已装箱。
    • 引用类型:已装箱。
  • 值类型派生自System.ValueType。该类型提供了与System.Object相同的方法,但是重写了EqualsGetHashCode方法,由于默认实现存在性能问题,自己定义的值类型应重写EqualsGetHashCode方法。
  • 值类型不能派生出其他类型。不应再值类型中引入新的虚方法。方法应是隐式密封的,不能时抽象的。
  • 变量:
    • 值类型:值类型变量包含其基础类型的一个值,值类型的所有成员初始化为0。
    • 引用类型:引用类型变量包含堆中对象的地址。引用类型的变量默认初始化为null。
  • 赋值:
    • 值类型:将值类型赋值变量给另外一个值类型变量,会逐字段复制。值类型变量自成一体,对值类型变量执行操作不会影响到另一个值类型变量。
    • 引用类型:将引用类型变量赋值给另一个引用类型变量,只复制内存地址。多个引用类型变量能引用堆中同一个对象,对一个变量执行操作,会影响另一个变量引用的对象。
  • 未装箱的值类型不分配到堆上,定义了该类型的一个实例的方法不再活动,为它们分配的存储就会被释放,不是等着进行垃圾回收。

CLR控制类型中的字段布局:CLR能按照它所选择的任何方式排列类型的字段。

C#编译器:

  • 引用类型:默认LayoutKind.Auto,让CLR自动排列字段。
  • 值类型:默认LayoutKind.Sequential,让CLR保持字段布局。

非托管union

  • union中的数据成员在内存中的存储相互重叠。
  • 每个数据成员都从相同的内存地址开始。
  • 存储的存储区数量是最大数据成员所需的内存数。
  • 同一时刻只有一个数据成员可以被赋值。

模拟非托管unionLayoutKind.Explicit,利用偏移量在内存中显式排列字段。

  • 引用类型和值类型的相互重叠时不合法的。
  • 值类型的相互重叠时合法的。

3. 值类型的装箱和拆箱

值类型比引用数据类型的原因:

  • 不作为对象在托管堆中分配。
  • 不被垃圾回收。
  • 不通过指针进行引用,不需要提领指针。

装箱(boxing):将值类型转换成引用类型。

装箱过程:

  1. 在托管堆中分配内存。分配内存量 = 各字段所需内存量 + 类型对象指针和同步块索引所需内存量。

    1. 值类型的字段复制到新分配的堆内存。
    2. 返回对象地址。

泛型集合类:允许在操作值类型的集合时,不需要对集合中的项进行装/拆箱。

拆箱(unboxing):获取已装箱实例中未装箱字段的地址称为拆箱。拆箱不要求在内存中复制任何字段,往往在拆箱后会将字段从堆复制到基于栈的变量中。

对象相等性(identity)同一性(equality)

  • 同一性(检查两个引用是否指向同一个对象):System.Object > static ReferenceEquals
  • 相等性(检查两个对象是否包含相同的值):重写Equals方法

重写Equals方法特征:

  • 自反性:x.Equals(x) -> true
  • 对称性:x..Equals(y) -> true, y.Equals(x) -> true
  • 可传递性:x.Equlas(y) -> true y.Equals(z) -> true, x.Equlas(z) -> true
  • 一致性:比较的两个值不变,返回结果不变。

4. 对象hash码

重写Equals方法必须也重写GetHashCode方法,以确保相等性算法和对象hash码算法一致。因为hash表集合HashTableDictionary…)中,要求两个对象必须具有相同的hash码才被视为相等。

自定义计算实例的hash码的算法应具有的特征:

  • 提供良好随机分配,使hash表获得最佳性能。
  • 可调用基类的GetHashCode方法,并包含它的返回值。一般不调用System.ObjectSystem.ValueTypeGetHashCode方法,不符合高性能hash算法。
  • 算法至少使用一个实例字段。
  • 理想情况,算法使用的字段应该不可变(immutable)。
  • 算法执行速度尽量快。
  • 包含相同值的不同对象应返回相同的hash码。

不要对hash码进行持久化,因为hash算法和hash码很容易被改变。

5. dynamic基元类型

类型安全的编程语言:所有表达式都解析成类型的实例,编译器生成的代码只执行对该类型有效的操作。

与非类型安全的语言相比,类型安全的语言优势:

  • 能在编译时检测代码的正确性。
  • 编译出更小、更快的代码:能在编译时进行更多预设,并在生成的IL和元数据中落实预设。

dynamic

  • 为了方便使用反射或者和其他组件进行通信,C#编译器允许将表达式的类型定义为dynamic。也可将表达式的结果放入变量,并将变量标记为dynamic

  • 代码使用dynamic表达式/变量调用成员时,编译器生成特殊的IL代码(payload,有效载荷)来描述所需的操作。

  • 在运行时,payload代码使用运行时绑定器(runtime binder)根据dynamic表达式/变量引用的对象的实际类型来决定具体执行操作。

所有表达式都能隐式转化为dynamicdynamic也可隐式转化为其他类型。


类型和成员基础

1. 类型的各种成员

成员名 说明
常量(const 1. 指出数据值恒定不变的符号。2. 使代码易于阅读和维护。3. 常量逻辑上总是静态成员,与类型关联,不与类型的实例关联。
字段(field 1. 表示只读或可读/写的数据值。2. 建议将字段声明为私有(private),防止类型/对象的状态被外部代码改变。3. 静态字段使类型状态的一部分;实例(非静态)字段是对象状态的一部分。
实例构造器(constructor 1. 将新对象的实例字段初始化为良好初始状态的特殊方法。
类型构造器(static constructor 1. 将类型的静态字段初始化为良好初始状态的特殊方法。
方法(method 1. 更改或查询类型/对象状态的函数。2.静态方法作用于类型;实例方法作用于对象。
操作符重载 1. 定义操作符作用于对象时,应该如何操作对象的方法
转换操作符 1. 定义如何隐式/显式将对象从一种类型转型为另一种类型的方法。
属性(property 1. 允许使用简单的、字段风格的语法设置或查询类型/对象的逻辑状态,同时保证状态不被破坏。2. 静态属性作用于类型;实例属性作用于对象。
事件(event 1. 静态事件:允许类型向一个或多个静态/实例方法发送通知。2. 实例事件:允许对象向一个或多个静态/实例方法发送通知。3. 事件包含两个方法:允许静态/实例方法登记注销对该事件的关注。4. 包含一个委托字段来维护已登记的方法集。
类型(class 1. 类型可定义其他嵌套类型。

2. 类型的可见性

C#编译器默认将类型的可见性标识为internal

友元程序集(firend assembly)

  • 使用[assembly:InternalsVisibleTo("xxx")]特性将其他程序集标识为自己的友元程序集。
  • 该特性获取标识友元程序集的名称和公钥字符串(不包含版本、语言文化、处理器架构信息)。
  • 友元程序集能访问该该程序集中的所有internal类型,以及这些类型的internal成员。

3. 成员的可访问性

C#编译器默认将成员的可见性标识为private

CLR要求接口(interface)类型的所有成员都具有public的可访问性,C#编译器禁止显示的指定接口成员的可访问性,自动将其设为public

派生类型重写基类型定义的成员时:

  • C#编译器:要求重写成员和原始成员有相同的可访问性。
  • CLR:允许重写成员放宽但不允许收紧原始成员的可访问性:因为CLR承诺派生类总能转型为基类,并获得对基类方法的访问权。
CLR术语 C#术语 描述
Private private 成员只能由定义类型/任何嵌套类型中的方法访问。
Family protected 成员只能由定义类型/任何嵌套类型/同一程序集或其他程序集中的派生类型中的方法访问。
Family and Assembly (不支持) 成员只能由定义类型/任何嵌套类型/同一程序集中的派生类型中的方法访问。
Assembly internal 成员只能由定义程序集中的方法访问。
Family or Assembly protected internal 成员只能由定义程序集/任何嵌套类型/同一程序集或其他程序集中的派生类型中的方法访问。
Public public 成员可由任何程序集的任何方法访问。

4. 静态类

静态类:

  • 永远不需要实例化的类。

  • 作用:组合一组相关成员。

  • C#使用static关键字定义不可实例化的类。

  • static关键字只能应用于类,不能应用于结构(值类型):因为CLR总是允许值类型初始化。

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

  • 静态类必须直接从System.Object类派生:继承只适用于对象,而静态类不能创建实例。
  • 静态类不能实现接口:因为只有使用类的实例时,才可调用类 的接口方法。
  • 静态类只能定义静态成员(字段、属性、方法、事件)。
  • 静态类不能作为字段、方法的参数、局部变量使用:因为它们都代表引用实例的变量。

5. 分部类/结构/接口

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

分散的原因:

  • 源代码控制。每个文件可单独check-out,同时编辑。
  • 在同一个文件中将类或结构分解成不同的逻辑单元。
  • 代码拆分。自己的代码和设计器生成的代码。

6. 组件、多态和版本控制

组件软件编程(Component Software Programming,CSP)特点:

  • 组件(.NET Framework成为程序集)有已经发布的意思。
  • 组件有自己的标识(名称、版本、语言文化、公钥)。
  • 组件永远维持自己的标识(程序集中的代码永远不会静态链接到另一个程序集中;.NET总是使用动态链接)。
  • 组件清楚指明它所依赖的组件(引用元数据表)。
  • 组件应编档它的类和成员。
  • 组件必须指定它所需要的安全权限(CLR的代码访问安全性(Code Access Security, CAS))。
  • 组件要发布在任何维护版本中都不会改变的接口/对象模型。

C#关键字及其对组件版本控制的影响:

C#关键字 作用于类型 作用于方法/属性/事件 作用于常量/字段
abstract 表示不能构造该类型的实例 表示为了构造派生类型的实例,派生类型必须重写并实现该成员 (不允许)
virtual (不允许) 表示该成员可由派生类型重写 (不允许)
override (不允许) 表示派生类型正在重写基类的成员 (不允许)
sealed 表示类型不能用作基类型 表示该成员不能被派生类型重写,只能将sealed应用于重写虚方法的方法 (不允许)
new 应用于嵌套类型:表示该成员与基类中相似的成员没任何关系 表示该成员与基类中相似的成员没任何关系 表示该成员与基类中相似的成员没任何关系

CLR如何调用虚方法、属性和事件:

  • 属性和事件实际作为方法实现。
  • 方法:代表在类型或类型的实例上执行某些操作的代码。所有方法都有名称、签名和返回类型(可为void)。
    • 静态方法:在类型上执行的操作。
    • 实例方法(非静态方法):在类型的实例上执行的操作。
      • 虚方法
      • 非虚实例方法
  • CLR允许类型定义多个同名方法,只要每个方法都有一组不同的参数或者一个不同的返回类型。C#不允许有只是返回类型不同的同名方法。

CLR方法调用指令:

  • call:该IL指令可调用静态方法、实例方法和虚方法。
    • 调用静态方法:必须指定方法的定义所在类的类型。
    • 调用实例方法/虚方法:必须指定引用了对象的变量,变量本身的类型指明了方法定义的类型。call指令假定该变量不为null。
  • callvirt:该IL指令可调用实例方法和虚方法,不能调用静态方法。
    • 调用实例方法:必须指定引用了对象的变量,变量的类型指明了方法定义的类型。
      • 调用实例方法:变量的类型指明了方法定义的类型。
    • 调用虚方法:必须指定引用了对象的变量。CLR调查发出调用的对象的实际类型,以多态的方式调用方法,为确定类型,会验证发出调用的变量不能为null。

设计类型时应尽量减少虚方法数量:

  • 调用虚方法的速度比调用非虚方法慢。
  • JIT编译器不能内嵌虚方法。
  • 虚方法使组件版本控制变得更脆弱。
  • 定义基类时,经常要提供一组重载的简便方法。

常量和字段

1.常量

常量是值从不变化的符号。

值必须能在编译时确定,编译器将常量值保存到程序集的元数据中。定义常量将导致创建元数据。所以只能定义编译器识别的基元类型的常量。

C#基元类型:Boolean, Char, Byte, SByte, Int16, UInt16, Int32, UInt32, Int64, UInt64, Single, Double, Decimal, String.

C#也允许定义值为null非基元类型常量变量

代码引用常量:

  1. 编译器在定义常量的程序集的元数据中查找该符号。
  2. 提取常量值嵌入生成的IL代码中。
    • 由于值直接嵌入代码,运行时不需要为常量分配内存。
    • 不能获取常量的地址,也不能以传引用的方式传递常量。

2. 字段

字段是一种数据成员,其中容纳了一个值类型的实例或对一个引用类型的引用。字段储存在动态内存中,值在运行时才能获取。

字段修饰符:

CLR术语 C#术语 说明
Static static 字段属于类型状态的一部分。
Instance (默认) 字段属于类型实例状态的一部分。
InitOnly readonly 1. 字段只能由构造器方法中的代码写入。2. 编译器和验证机制确保readonly字段不会被构造器之外的任何方法写入(可利用反射修改readonly字段)。3. readonly标记的引用类型字段,不可变的是引用,不是引用对象。
Volatile volatile 编译器、CLR、硬件不会对访问该字段的代码执行“线程不安全”的优化措施。

CLR支持:

  • 类型(静态)字段:
    • 引用类型的方法进入JIT编译时,将类型加载到AppDomain中,创建类型对象,在内存对象中为静态字段分配动态内存。
  • 实例(非静态)字段

内联初始化:在代码中直接赋值来初始化。C#实际是在构造器中对字段进行的初始化,内联初始化是语法上的简化。


方法

1. 实例构造器和类(引用类型)

构造器:将类型实例初始化为良好状态的特殊方法。构造器在方法定义元数据表.ctor

创建引用类型实例时:

  1. 为实例数据字段分配内存
  2. 初始化对象的附加字段(overhead fields, 类型对象指针和同步块索引)
  3. 调用类型的实例构造器设置对象的初始状态

不调用实例构造器,创建类型的实例:

  • Object > MemberwiseClone():作用是分配内存,初始化对象的附加字段,然后将源对象的字节数据复制到新对象中。
  • 运行时序列化器(runtime serializer)反序列化对象:使用FormatterServices > GetUninitializedObject()/GetSafeUninitializedObject()为对象分配内存。

调用构造器:

  1. 初始化内联初始化的字段
  2. 调用基类构造器
  3. 执行构造器自己的代码

2. 实例构造器和结构(值类型)

  • CLR总是允许创建值类型的实例,并且无法阻止值类型的实例化。所以值类型不需要定义构造器,C#编译器也不会为值类型生成默认的无参构造器。
  • C#编译器不允许值类型定义无参构造器,但CLR允许。
  • CLR允许为值类型定义构造器,但必须显示调用才会执行。
  • 不能在值类型中内联初始化实例字段,可以内联初始化静态字段。
  • 值类型的构造器必须初始化值类型的全部字段。

3. 类型构造器

类型构造器(type constructor):又称静态构造器(static constructor)、类构造器(class constructor)、类型初始化器(type initializer)。类型构造器在方法定义元数据表.cctor(class constructor)

类型构造器:

  • 可用于接口(C#编译器不允许)、引用类型和值类型。
  • 类型默认没有定义类型构造器。
  • 只能定义一个类型构造器。
  • 类型构造器只能使私有的、无参的。
  • 类型构造器的调用由CLR负责。
  • 类型构造器只能访问类型的静态字段。

作用:

  • 实例构造器:设置类型的实例的初始状态。
  • 类型构造器:设置类型的初始状态。

4. 操作符重载方法

对于CLR而言,操作符重载只是方法而已。CLR规范要求操作符重载方法必须是public static方法。

C#要求操作符重载方法至少有一个参数的类型当前定义这个方法的类的类型相同。为了使C#编译器能在合理的时间内找到要绑定的操作符方法。

  • C#使用operator关键字重载操作符方法

  • 元数据使用specialname关键字标识操作符重载方法

public sealed class Complex
{
    punlic static Complex operator+(Complex c1, Complex c2)
    { 
        //... 
    }
}

C#一元操作符:

C#操作符 特殊方法名 推荐相容于CLS的方法名(友好名称)
+ op_UnaryPlus Plus
- op_UnaryNegation Negate
! op_LogicalNot Not
~ op_OnesComplement OnesComplement
++ op_Increment Increment
op_Decrement Decrement
(无) op_True IsTrue{get;}
(无) op_False IsFalse{get;}

C#二元操作符:

C#操作符 特殊方法名 推荐相容于CLS的方法名(友好名称)
+ op_Addition Add
- op_Subtraction Subtract
* op_Multiply Multiply
/ op_Division Divide
% op_Modulus Mod
& op_BitwiseAnd BitwiseAnd
| op_BitwiseOr BitwiseOr
^ op_ExclusiveOr Xor
« op_LeftShift LeftShift
» op_RightShift RightShift
== op_Equality Equals
!= op_Inequality Equals
< op_LessThan Compare
> op_GreaterThan Compare
<= op_LessThanOrEqual Compare
>= op_GreaterThanOrEqual Compare

5. 转换操作符方法(转换构造器和方法)

转换操作符:是将一个对象从一种类型转换为另一种类型的方法。CLR规范要求转换操作符重载方法必须是public static方法。

C#要求参数的类型返回类型二者必有其一与定义转换方法的类的类型相同。为了使C#编译器能在合理的时间内找到要绑定的操作符方法。

  • 定义只有一个参数的公共构造器,该参数要求是源类型的实例。
  • 定义无参的公共实例方法ToXXX,将定义类型的实例转换为XXX类型。
public static  class Rational
{
    public Rational(int num)
    {
        //...由int构造Rational
    }
    
    punlic int ToInt()
    {
        //...将Rational转换为int
    }
    
    //由int隐式构造并返回Rational
    public static implicit operator Rational(int num)
    {
        return new Rational(num);
    }
    
    //由Rational显式返回int
    public static explicit operator int(Rational r)
    {
        return r.ToInt();
    }
}

public sealed class Program
{
    public static void Main()
    {
        Rational r = 5; //int隐式转换为Rational
        int x = (int)r; //Rational显式转换为int
    }
}

6. 扩展方法

C#扩展方法:允许定义静态方法,并使用实例方法的语法来调用。方法的第一个参数前加this关键字。

public static class StringBuilderExtensions
{
    public static int IndexOf(this StringBuilder sb, Char value)
    {
        //...
    }
}

//call
StringBuilder sb =  new StringBuilder("Hello. My name is Jordon.");
sb.Replace('.', '!').IndexOf('J');

规则和原则:

  • C#只支持扩展方法,不支持扩展属性、事件、操作符等。
  • 扩展方法(第一个参数前有this的方法)必须在非泛型的静态类中声明(类名没有限制)。方法必须至少有一个参数,只能第一个参数前能用this关键字标识。
  • C#编译器在静态类中查找扩展方法时,要求静态类本身必须具有整个文件的作用域(静态类不能嵌套在另一个类中)。
  • 因为静态类名无限制,C#编译器要花一定时间来寻找扩展方法,为增强性能,所以C#编译器要求使用using关键字导入扩展方法所在类的命名空间。
  • 多个静态类可以定义相同的扩展方法,编译器检测到多个扩展方法会报错。不能再使用实例方法语法调用,要使用静态方法语法
  • 用一个扩展方法扩展一个类型的同时也扩展了它的派生类型。
  • 扩展方法实质是对一个静态方法的调用,所以CLR不会生成代码对调用方法的表达式的值进行null检查。
  • 扩展方法可能存在版本控制问题。

ExtensionAttribute:C#中,一旦用this关键字标记了静态方法的第一个参数,编译器就会在内部向该方法应用ExtensionAttribute特性,该特性会在最终生成的文件的元数据中持久性的存储下来。

7. 分部(partial)方法

  • 没有实现分部方法,编译器不会生成任何代表分部方法的元数据。编译器不会生成对本该传给分部方法的实参进行求值的IL指令。

规则和原则:

  • 分部方法只能再分部类或结构中声明。
  • 分部方法的返回类型始终是void,任何参数都不能用out修饰符来标记。因为分部方法运行时可能不存在。
  • 分部方法的声明和实现必须具有完全一致的签名。
  • 如果没有对应的实现部分,便不能在代码中创建委托来引用分部方法。
  • 分部方法总是被视为private方法。但C#编译器禁止在分部方法声明添加private关键字。

参数

1. 可选参数和命名参数

可选参数:在设计方法时,可以为部分或全部参数分配默认值。

可选参数规则和原则:

  • 可以为方法、构造器方法和有参属性(C#索引器)的参数指定默认值。还可为委托定义一部分的参数指定默认值。
  • 有没默认值的参数必须放在没有默认值的所有参数之后。
  • 默认值必须是编译时能确定的常量值。
  • 不要重命名参数变量。
  • 如果方法从模块外部调用,更改参数的默认值具有潜在危险。
  • 参数使用了out/ref关键字进行标记,就不能设置默认值。因为无法为其传递有意义的默认值。

使用可选或命名参数调用方法时,规则和原则:

  • 实参可按任意顺序传递,但命名实参只能出现在实参列表尾部。
  • 可按名称将实参传给没有默认值的参数,但所有必须的实参都必须传递。
  • C#不允许省略逗号之间的实参。

DefaultParameterValueAttributeOptionalAttribute

  • 在C#中,一旦为参数分配了默认值,编译器就会在内部向该参数应用OptionalAttribute特性,该特性会在最终生成的文件的元数据中持久性的存储下来。
  • 此外,编译器向参数应用DefaultParameterValueAttribute特性,并将该特性持久性的存储到最终生成的文件的元数据中。
  • 然后,会向DefaultParameterValueAttribute的构造器传递指定的默认值。

2. 隐式类型的局部变量(var)

var:C#能根据初始化表达式的类型推断方法中的局部变量的类型。

  • 只能声明方法内部的局部变量,不能声明类型的字段。

  • 不能将null赋值给隐式类型的局部变量,因为null能隐式转换为任何引用类型或可空值类型,编译器无法推断出它的确切类型。

  • 不能用var声明方法的参数类型。

3. 以引用的方式向方法传递参数(ref、out)

CLR默认所有方法参数都是传值的。CLR允许以传引用而非传值的方式传递参数。

C#使用outref关键字,编译器将生成代码来传递参数的地址,而非传递参数本身。

CLR不区分outref关键字,都会生成相同的IL代码,都导致传递指向实例的一个指针。

  • out:不要求在调用方法前初始化,被调用的方法不能读取参数的值,在返回前必须向这个参数写入值。

    • 为大的值类型使用out,可以提高代码的执行效率,因为它避免了在进行方法调用时复制值类型的实例字段。
  • ref:需要在调用该方法前初始化参数的值,被调用的方法可以读写该参数的值。

栈帧(stack frame):代表当前线程的调用栈中的一个方法的调用。

以传引用的方式传给方法的变量,变量的类型必须与方法签名中声明的类型相同,以保障类型的安全。

4.向方法传递可变数量的参数

params

  • 编译器会向参数应用定制特性System.ParamArrayAttribte的一个实例。
  • 只能应用于方法签名中的最后一个参数。
  • 这个参数只能标识一维数组(任意类型)。

params影响性能:

  • 数组对象必须在堆上分配内存。
  • 数组元素必须初始化。
  • 数组内存最终需要垃圾回收。

推荐:

  • 定义几个常规情况下的没有params关键字的重载方法。

  • 定义一个不常见情况下含有params关键字重载方法。

5. 参数和返回类型的设计规范

  • 方法的参数类型:尽量指定最弱的类型。
    • 接口体系结构
    • 基类体系结构
  • 方法的返回类型:声明为最强的类型。

6. 常量性

CLR不支持常量的对象/实参。


属性

属性:允许源代码用简化语法来调用方法。

CLR支持两种属性:

  • 无参属性:通常称的属性。get访问器方法不接受参数。
  • 有参属性:索引器。get访问器方法接受一个或多个参数,set访问器方法接受两个或多个参数。

1. 无参属性

面向对象设计和编程的重要原则之一:数据封装

副作用(side effect):如果一个函数或表达式除了生成一个值,还会造成状态的改变,就说它会造成副作用,或者说会执行一个副操作。

访问器(accessor)方法:封装了字段访问的方法。访问器方法可选择对数据的合理性进行检查,确保对象的状态永远不被破坏。set访问器包含一个隐藏参数value

智能字段:可以把属性(property)想成智能字段,即背后有额外逻辑的字段。

属性的唯一好处:提供了简化语法。

支持字段:即私有字段。

定义属性时,编译器在托管程序集中生成:

  • 代表属性get访问器的方法。仅在定义了get访问器方法时生成。
  • 代表属性set访问器的方法。仅在定义了set访问器方法时生成。
  • 托管程序集元数据中的属性定义。这一项必然生成。
    • 包含一些标志(flags)和属性类型,还引用了get和set访问器方法。
    • 作用:在属性这种抽象概念与它的访问器方法之间建立起一个联系。
    • 编译器利用这种元数据信息。CLR不使用这种元数据信息,在运行时只需要访问器方法。
自动实现的属性

自动实现的属性(Automatically Implemented Property, AIP):声明属性而不提供get/set方法的实现,C#会自动声明一个私有字段,并自动实现get/set方法,分别返回和设置到字段中的值。

合理定义属性

属性看起来和字段相似,但本质是方法。

  • 属性可以只读/只写,而字段访问总是可读/可写(readonly字段只在构造器中可写)。定义属性,最好同时提供get和set方法。
  • 属性方法可能抛出异常,字段访问永远不会。
  • 属性不能作为out/ref参数传给方法,字段可以。
  • 属性方法可能花费较长时间执行,字段访问总是立即完成。
  • 连续多次调用,属性方法每次都可能返回不同的值(DateTime.Now, Environment.TickCount),字段则每次都返回相同的值。
  • 属性方法可能造成明显的副作用(side effect),字段访问永远不会。
  • 属性方法可能需要额外的内存,或者返回的引用并非指向对象状态的一部分,造成对返回对象的修改作用不到原始对象身上。查询字段返回的引用总是指向原始对象状态的一部分。
对象和集合初始化器

对象的初始化:替换(replacement)操作。

集合的初始化:相加(additive)操作

对象初始化器语法的好处:允许在表达式的上下文(相对于语句的上下文)中编码,允许组合多个函数,进而增强了代码的可读性。

Employee e = new Employee { Name = "Jordon", Age =24 }.ToString().ToUpper();

如果属性的类型实现了IEnumerable/IEnumerable<T>接口,属性就被认为是集合。

var table = new Dictionary<string, int> {
    {"Jordon", 24}, {"Vicky", 28}  
};
匿名类型

匿名类型:可以用很简洁的语法来自动声明不可变(immutable)的元组类型。

元组类型:含有一组属性的类型,这些属性以某种方式相互关联。

var o = new { prop1 = expression1, ... };

编译器过程:

  • 推断每个表达式类型,创建推断类型的私有字段
  • 为每个字段创建公共只读属性(防止对象的hash码发生改变),并创建构造器接受所有表达式。
  • 在构造器中,使用传给它的表达式的求值结果来初始化私有只读字段。
  • 重写Object的Equals, GetHashCode, ToString方法,并生成方法中的代码。
System.Tuple类型

元数(arity):一个函数或者运算(操作)的元数是指函数获取的实参或者操作数的个数。

System.Tuple:从Object派生,泛型参数的个数不同(元数不同)。Tuple创建好后就是不可变的。

public class Tupel<T1, ...>
{
	private T1 m_Item1;
    //...
    public Tupe(T1 item1) { m_Item1 = item1; }
    //...
    public T1 Item1 { get { return m_Item1; } }
    //...
}

匿名类型:属性的实际名称是根据定义匿名类型的源代码来确定。

Tuple类型:属性一律被Microsoft定义为Item#,无法更改。

2. 有参属性

C#使用数组风格的语法公开有参属性(索引器),对[]操作符的重载。

索引器:

  • 至少有一个参数,可以有多个。
  • 参数和返回值类型可以是除了void外的任意类型。
  • C#将this[...]作为表达索引器的语法。不允许指定索引器名称。可使用IndexerNameAttribute重命名这些方法。
public sealed class SomeType
{
    private int num;
    public int this[bool b]
    {
        get { return 0; }
        set { if(b) { num = value; } }
    }
}

关联数组(associative array):使用字符串索引(键)来访问存储在数组中的值(值)。

CLR本身不区分无参属性和有参属性(索引)。对于CLR,每个属性都是类型中定义的一堆方法和一些元数据。

3. 调用属性访问器方法时的性能

对于简单的get/set访问器方法,JIT编译器会把代码内联/嵌入(inline)。这样,使用属性就没有性能上的损失。

内联(inline):是指将方法的代码直接编译到调用它的方法中。

4. 属性访问器的可访问性

如果两个访问器方法需要不同的可访问性,C#要求必须为属性本身指定限制最小的可访问性。两个访问器智能选择一个使用限制较大的。

5. 泛型属性访问器

C#不允许属性引入自己的泛型参数,因为属性时用来表示可供查询或设置的某个对象特征,一旦引入泛型参数,就有可能改变查询/设置行为。但属性不是行为。


事件

定义了事件成员的类型允许类型(或类型的实例)通知其他对象发生了特定的事情。

CLR事件模型以委托为基础。委托是调用(invoke)回调方法的一种类型安全的方式,对象凭回调方法接收它们订阅的通知。

定义了事件成员的类型提供功能:

  • 方法能登记对事件的关注。
  • 方法能注销对事件的关注。
  • 事件发生时,登记了的方法将收到通知。(类型维护了一个已登记方法列表,事件发生后,类型将通知列表中所有已登记方法。)

1. 设计要公开事件的类型

  • step 1:定义类型来容纳所有需要发送给事件通知接收者的附加信息(EventArgs)。

  • step 2:定义事件成员(event)。

    public event EventHandler<xxxEventArgs> EventName;
    //public 可访问性标识符
    //event C#事件关键字
    //EventHandler<xxxEventArgs> 委托类型,指出要调用方法的原型。
    //public delegate void EventHandler<TEventArgs>(Object sender, TEventArgs e);
    //所有接收者都必须提供一个原型和委托类型匹配的回调方法。
    //EventName 事件名称
    
    • sender参数定义为Object原因:1.继承 2. 灵活性。
    • 事件模型要求委托定义和回调方法将派生自EventArgs的参数命名为e,加强事件模型的一致性。
    • 事件模型要求所有事件处理程序event handler的放回类型都是void,因为引发事件后可能要调用好几个回调方法,但没办法获得所有方法的返回值。
  • step 3:定义负责引发事件的方法(protected virtual void Onxxx(){})来通知事件的登记对象。

  • step 4:定义方法将输入转化为期望事件。

2. 编译器如何实现事件

public event EventHandler<xxxEventArgs> EventName;
//-------C#编译器------------
//1. 一个被初始化为null的私有委托字段,是对一个委托列表的头部的引用。
//private是防止类外部的代码不正确地操作它。
private EventHandler<xxxEventArgs> EventName = null;
//2. 一个公共的add方法,允许方法登记对事件的关注
public void add_EventName(EventHandler<xxxEventArgs> value)
{
    //调用System.Delegate的静态Combine方法,将委托实例添加到委托列表,
    //返回新的列表头,并将这个地址存回字段。
}
//3. 一个公共的remove方法,允许方法注销对事件的关注
public void remove_EventName(EventHandler<xxxEventArgs> value)
{
    //调用System.Delegate的静态Remove方法,将委托实例从委托列表中删除,
    //返回新的列表头,并将这个地址存回字段。
}
//4. 托管程序集的元数据中生成事件定义的记录项(flag、基础委托类型、引用add和remove访问器方法)

3. 设计侦听事件的类型

  • C#要求使用+=操作符实现对事件的关注的登记(在列表中增加委托),使用-=操作符实现对事件关注的注销(在列表中删除委托)。

4. 显式实现事件

C#编译器允许开发人员显式的实现一个事件,使开发人员能够控制add/remove方法处理回调委托的方式。


泛型

代码重用:1. 类型重用 2. 算法重用

泛型(generic):是CLR和编程语言提供的一种特殊的机制,支持算法重用。

CLR允许创建泛型引用类型泛型值类型,但不允许创建泛型枚举类型

CLR还允许创建泛型接口泛型委托。CLR允许在引用类型、值类型和接口中定义泛型方法。

<T>:表示操作的是一个未指定的数据类型。

  • 类型参数(type parameter):在定义泛型类型或方法时,为类型指定的任何变量都称为类型参数。
  • 类型实参(type argument):使用泛型类型或方法时指定的具体数据类型称为类型实参。

Microsoft的设计原则:泛型参数变量要么称为T,要么至少以大写T开头。

泛型优势:

  • 源代码保护:使用泛型算法的开发人员不需要访问算法源码。
  • 类型安全:保证只有与指定数据类型兼容的对象才能用于算法。
  • 更清晰的代码:由于编译器强制类型安全性,减少了强制类型转换。
  • 更佳的性能:1. 使用值类型的泛型算法,CLR不需要执行 任何装箱操作。2. 由于不需要强制类型转换,CLR无需验证类型转换是否类型安全。

1. FCL中的泛型

Microsoft建议使用泛型集合类,不建议使用非泛型集合类

  • 泛型集合类有类型安全、代码清晰以及更佳的性能的优点。
  • 泛型类具有更好的对象模型(虚方法数量显著变少,性能更好)。
  • 泛型集合类增加了一些新成员。

2. 泛型基础结构

CLR内部如何处理泛型?

1. 开放类型和封闭类型

开放类型:具有泛型类型参数的类型称为开放类型,CLR禁止构造开放类型的任何实例。

封闭类型:为所有类型参数都传递了实际的数据类型,类型就成为了封闭类型。CLR允许构造封闭类型的实例。

2. 泛型类型和继承

使用泛型类型并指定类型实参时,实际是在CLR中定义一个新的类型对象,新的类型对象从泛型类型派生自的那个类型派生。

指定类型实参不影响继承层次结构。

3. 泛型类型同一性

类型的同一性(identity)相等性(equivalence)

C#允许使用简化语法(using)引用泛型封闭类型,同时不影响类型的相等性:

using DateTimeList  = System.Collections.Generic.List<System.DateTime>;
4. 代码爆炸

代码爆炸:使用泛型类型参数的方法在进行JIT编译时,CLR要为每种不同的 方法/类型组合生成本机代码。可能造成应用程序的工作集显著增大,损害性能。

CLR内建优化措施:

  • 为特定的类型实参调用了一个方法,之后再用相同类型实参调用这个方法,CLR只会为这个方法/类型组合编译一次。
  • CLR认为所有引用类型实参都完全相同,实现代码共享。因为所有引用类型的实参或变量实际只是指向堆上对象的指针,所有对象指针都以相同的方式操作。

3. 泛型接口

CLR提供对泛型接口的支持:

  • 引用类型和值类型可以指定类型实参来实现接口。
  • 也可保持类型实参的未指定状态来实现泛型接口。

4. 泛型委托

CLR支持泛型委托:

  • 保证任何类型的对象都能以类型安全的方式传给回调方法。
  • 泛型委托允许值类型实例再传给回调方法时不进行任何装箱操作。

委托本质是提供了4个方法的一个类的定义:

  • 构造器
  • Invoke
  • BeginInvoke
  • EndInvoke

建议尽量使用FCL预定义的泛型:ActionFunc委托。

5. 委托和接口的逆变和协变泛型类型实参

委托的每个泛型类型参数都可以标记为协变量逆变量

泛型类型参数形式:

  • 不变量(invariant):意味着泛型类型参数不能更改。

  • 逆变量(contracariant):意味着泛型类型参数可以从一个类更改为它的某个派生类,指定参数的兼容性。

    • C#使用in关键字标记逆变量形式的泛型类型参数。
    • 逆变量泛型类型参数只能出现在输入位置(方法参数…)。
  • 协变量(covariant):意味着泛型类型参数可以从一个类更改为它的某个基类,指定返回类型的兼容性。

    • C#使用out关键字标记协变量形式的泛型类型参数。
    • 协变量泛型类型参数只能出现在输出位置(方法返回值…)。

对于泛型类型参数,如果将该实例的实参传给使用out/ref关键字的方法,便不允许可变性(in/out)。

使用要获取泛型参数和返回值的委托时,应尽量为逆变性和协变性指定in/out发、关键字。

6. 泛型方法

CLR允许方法指定自己的类型参数。

类型推断:为了改进代码创建,增强可读性和可维护性,C#编译器支持在调用泛型方法时进行类型推断。编译器会在调用泛型方法时自动判断要使用的类型。

推断类型时,C#使用变量的数据类型,而不是变量引用的对象的实际类型。

private static void Swap<T>(ref T o1, ref T o2)
{
    //....
}

int n1 = 1, n2 = 2;
string s1 = "Jordon";
Object s2 = "Vicky";
Swap<int>(ref n1, ref n2);
Swap(ref n1, ref n2); //类型推断
Swap(ref s1, ref s2); //error,不能有推断类型。

C#编译器策略:优先考虑较明确的匹配,再考虑泛型匹配。

7. 泛型和其他成员

C#中,属性、索引器、事件、操作符方法、构造器和终结器本身不能有类型参数。但是它们能在泛型类型中定义,而且这些成员中的代码能使用类型的类型参数。

C#之所以不允许这些成员指定自己的泛型类型参数:

  • 开发人员很少需要将这些成员作为泛型使用。
  • 为这些成员添加泛型支持代价太高。

8. 可验证性和约束(where)

编译泛型代码时,C#编译器会进行分析,确保代码适用于当前已有或将来可能定义的任何类型。编译器和CLR支持称为约束的机制。

约束的作用:限制能指定成泛型实参的类型数量。编译器负责保证类型实参符合指定的约束。

约束可用于:

  • 泛型类型的类型参数。
  • 泛型方法的类型参数。

CLR不允许基于类型参数名称或约束的重载;只能基于元数(类型参数个数)对类型或方法进行重载。

  • 重写虚泛型方法时,重写方法必须指定相同数量的类型参数,而且类型参数会继承基类方法上的约束。
  • 重写方法的类型参数不允许指定任何约束,但类型参数名称可变。

编译器/CLR允许向类型参数应用的约束:

  • 主要约束
  • 次要约束
  • 构造器约束
主要约束

类型参数都可以指定零个或一个主要约束。

主要约束:

  • 代表非密封类的一个引用类型:指定引用类型约束时,向编译器承诺:指定的类型实参的类型要么与约束类型相同,要么派生自约束类型。

    • 不能指定特殊的引用类型:Object, Array, Delegate, MulticastDelegate, ValueType, Enum, Void
    • 如果类型参数没有指定主要约束,默认使Object,但不能显式指定Obejct,否则报错。
  • class:向编译器承诺:类型实参是引用类型。

    xxx<T> where T : class {}
    
  • struct:向编译器承诺:类型实参是值类型。

    xxx<T> where T : struct {}
    
    • 编译器和CLR将Nullable<T>值类型视为特殊类型,不满足struct约束。因为Nullable<T>类型将自身的类型参数约束为struct,而CLR希望禁止像Nullable<Nullable<T>>这样的递归类型。
次要约束

类型参数可以指定零个或多个次要约束。

次要约束:

  • 代表接口类型:向编译器承诺:类型参数实现了接口。
    • 由于能指定多个接口,所以类型参数必须实现了所有接口约束(以及主要约束)。
  • 类型参数约束(裸类型约束):允许一个类型或方法规定:指定的类型实参要么是约束类型,要么是约束类型的派生类。
构造器约束

类型参数可以指定零个或一个构造器约束。

构造器约束:

  • 向编译器承诺:实参是实现了公共无参构造器的非抽象类。

    xxx<T> where T : new() {}
    
    • 不能和struct约束一起用,因为所有值类型都隐式提供了公共无参构造器,造成多余。
其他可验证性问题
  • 泛型类型变量的转型

    将泛型类型的变量转型为其他类型是非法的,除非为与约束兼容的类型。

  • 为泛型类型变量设默认值

    使用default关键字,如果泛型类型变量为引用类型,默认值为null,值类型为0

    private static void MethodName<T>() 
    {
        T temp = default(T);
    }
    
  • 将泛型类型变量与null比较

    无论反泛型类型是否被约束,使用==/!=操作符将泛型类型变量与null比较都是合法的。

  • 两个泛型类型变量比较

    如果泛型类型参数不能肯定是引用类型,对同一个泛型类型的两个变量进行比较是非法的。

  • 泛型类型变量作为操作数使用

    C#不能将操作符用于泛型类型的变量。


接口

1. 类和接口继承

基类:提供了一组方法签名和这些方法的实现。

接口:只是对一组方法签名的统一命名,不提供任何实现。

类继承特点:凡是能使用基类型实例的地方,都能使用派生类型的实例。

接口继承特点:凡是能使用具名接口类型的实例的地方,都能使用实现了接口的一个类型的实例。

2. 定义接口(interface)

接口不能定义:构造器方法、实例字段、静态成员。

在CLR看来,接口定义就是类型定义。

3. 继承接口

C#编译器要求:将实现接口的方法(接口方法)标记为public

CLR要求:将接口方法标记为virtual

  • 不显示标记为virtual,编译器默认标记为virtual sealed,这会阻止派生类重写接口方法。

  • 显式标记为virtual,编译器就会将方法标记为virtual并保持其非密封状态,使派生类能重写它。

4. 关于调用接口方法的更多探讨

CLR允许定义接口类型的 字段、参数或局部变量。使用接口类型的变量可以调用该接口中定义的方法。

值类型可以实现零个或多个接口。但值类型在转化为接口类型时必须装箱。

5. 隐式和显式接口方法实现(幕后的事)

类型加载到CLR时,会为该类型创建并初始化一个方法表:

  • 类型引入的每个新方法的记录项。
  • 继承的所有虚方法的记录项。
    • 基类定义的方法。
    • 接口类型定义的方法。

显式接口方法实现(EIMI)

  • C#中,将定义方法的那个接口的名称作为方法名前缀,就会创建EIMI。
  • C#不允许在定义显式接口方法时指定可访问性(编译器自动生成private),也不能标记为virtual,所以不能被重写。
  • EIMI并非是类型的对象模型的一部分,只是将接口和类型连接起来。
internal sealed class SimpleType : IDisposable 
{
    public void Dispose() {}
    //EIMI
    void IDisposable.Dispose() {}
}

6. 泛型接口

泛型接口优点:

  • 提供出色的编译时类型安全。

  • 处理值类型时装箱次数会少很多。

  • 一个类型可以实现同一个接口若干次,只要每次使用不同的类型参数。

    public sealed class ClassName : IComparable<int>, IComparable<string> {}
    

7. 泛型和接口约束

将泛型类型参数约束为接口的好处:

  • 可将泛型参数约束为多个接口。
  • 传递值类型的实例时减少装箱。C#编译器为接口约束生成特殊IL指令,导致直接在值类型上调用接口方法而不装箱。

8. 实现多个具有相同方法名和签名的接口

使用显式接口方法实现来实现,加以区分。

9. 用显式接口方法实现来增强编译时类型安全性

10. 谨慎使用显式接口方法实现

EIMI问题:

  • 没有文档解释类型具体如何实现一个EIMI方法,对类型的转换要求不明确。
  • 值类型的实例在转换为接口时装箱。
  • EIMI不能由派生类型调用。

11. 设计:基类还是接口?

设计规范:

  • IS-A对比CAN-DO关系:IS-A:基类,属于。CAN-DO:接口,能做某事。
  • 易用性
  • 一致性实现
  • 版本控制

「下次一定」

下次一定

使用微信扫描二维码完成支付