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
操作符运行过程:
- 计算所需要的字节数:
- 类型及其所有基类的实例字段所需要的字节数
- 成员管理对象:
类型对象指针(type object pointer)
和同步块索引(sync block index)
所需要的字节数
- 从托管堆中分配内存空间,分配的所有字节都设为0。
- 初始化对象的
类型对象指针
和同步块索引
成员。 - 调用类型的构造器,传递指定的实参。自动调用基类构造器,构造器负责初始化类型的实例字段。
- 返回指向新建对象的一个引用(指针)。
2. 类型转换
CLR最重要的特性之一是类型安全
。
- 隐式转换
- 显式转换
C#is
操作符:检查对象是否兼容于指定类型,返回true
或false
。
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)代码:在方法完成工作和对其进行清理,以便返回至调用者。
方法调用:
- 将实参地址压入栈。
- 将方法返回地址压入栈。
- 序幕代码为局部分配内存空间。
- 执行代码。
- return 将CPU的指令指针设置成栈中的返回地址。
- 方法的栈帧展开(unwind)。栈帧:当前线程的调用栈中的一个方法调用。
围绕CLR方法调用:
- JIT将IL代码转换为CPU指令时,CLR会确认引用的所有类型的程序集已经加载,利用程序集的元数据,提取与类型有关的信息,在托管堆中创建
类数据结构(类型对象)
来表示类型本身。
类型对象
:类型对象指针(type object pointer)+同步块索引(sync block index)+静态字段+方法表…- 静态数据字段分配在类型对象自身中。
-
CLR确认方法所需的所有类型对象都已创建,代码已经编译完成后,执行本机代码。
-
序幕代码在线程栈中为局部变量分配内存,CLR自动为局部变量初始化。
-
在托管堆中创建类型的实例对象:类型对象指针+同步块索引+(本身和基类的)实例数据字段。
-
CLR自动初始化类型对象指针指向对象对应的
类数据结构(类型对象)
。 -
在调用类型构造器之前,CLR初始化同步块索引,并初始化对象的实例字段。
-
new操作符返回对象的内存地址,保存到线程栈上的变量中。
-
-
调用静态方法:
- CLR定位与定义静态方法的类型对应的
类型对象
。 - JIT编译器在
类型对象
的方法表中查找与被调用方法对应的记录项,对方法进行JIT编译,调用编译后的代码。
- CLR定位与定义静态方法的类型对应的
-
调用非虚实例方法:
- JIT编译器找到调用的变量的类型对应的
类型对象
,查找或回溯查找被调用方法的记录项 - JIT编译器在
类型对象
的方法表中查找与被调用方法对应的记录项,对方法进行JIT编译,调用编译后的代码。
- JIT编译器找到调用的变量的类型对应的
-
调用实例方法:
- 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),字面值可被看成类型本身的实例,调用实例方法。
checked
和unchecked
基元类型操作:
- 对基元类型执行算术运算时可能造成溢出。
- CLR提供一些特殊IL指令,允许编译器选择对溢出的处理行为。
-
add/add.ovf, sub/sub.ovf, mul/mul.ovf, conv/conv.ovf
-
- C#编译器溢出检查默认是关闭的。
- 全局打开:
/checked+
编译器开关 - 特定区域检查:
checked
和unchecked
操作符
- 全局打开:
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
相同的方法,但是重写了Equals
和GetHashCode
方法,由于默认实现存在性能问题,自己定义的值类型应重写Equals
和GetHashCode
方法。 - 值类型不能派生出其他类型。不应再值类型中引入新的虚方法。方法应是隐式密封的,不能时抽象的。
- 变量:
- 值类型:值类型变量包含其基础类型的一个值,值类型的所有成员初始化为0。
- 引用类型:引用类型变量包含堆中对象的地址。引用类型的变量默认初始化为null。
- 赋值:
- 值类型:将值类型赋值变量给另外一个值类型变量,会逐字段复制。值类型变量自成一体,对值类型变量执行操作不会影响到另一个值类型变量。
- 引用类型:将引用类型变量赋值给另一个引用类型变量,只复制内存地址。多个引用类型变量能引用堆中同一个对象,对一个变量执行操作,会影响另一个变量引用的对象。
- 未装箱的值类型不分配到堆上,定义了该类型的一个实例的方法不再活动,为它们分配的存储就会被释放,不是等着进行垃圾回收。
CLR控制类型中的字段布局:CLR能按照它所选择的任何方式排列类型的字段。
C#编译器:
- 引用类型:默认
LayoutKind.Auto
,让CLR自动排列字段。 - 值类型:默认
LayoutKind.Sequential
,让CLR保持字段布局。
非托管union
:
union
中的数据成员在内存中的存储相互重叠。- 每个数据成员都从相同的内存地址开始。
- 存储的存储区数量是最大数据成员所需的内存数。
- 同一时刻只有一个数据成员可以被赋值。
模拟非托管union
:LayoutKind.Explicit
,利用偏移量在内存中显式排列字段。
- 引用类型和值类型的相互重叠时不合法的。
- 值类型的相互重叠时合法的。
3. 值类型的装箱和拆箱
值类型比引用数据类型轻
的原因:
- 不作为对象在托管堆中分配。
- 不被垃圾回收。
- 不通过指针进行引用,不需要提领指针。
装箱(boxing)
:将值类型转换成引用类型。
装箱过程:
-
在托管堆中分配内存。分配内存量 = 各字段所需内存量 + 类型对象指针和同步块索引所需内存量。
- 值类型的字段复制到新分配的堆内存。
- 返回对象地址。
泛型集合类
:允许在操作值类型的集合时,不需要对集合中的项进行装/拆箱。
拆箱(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表集合
(HashTable
、Dictionary
…)中,要求两个对象必须具有相同的hash码才被视为相等。
自定义计算实例的hash码的算法
应具有的特征:
- 提供良好随机分配,使
hash表
获得最佳性能。 - 可调用基类的
GetHashCode方法
,并包含它的返回值。一般不调用System.Object
或System.ValueType
的GetHashCode
方法,不符合高性能hash算法。 - 算法至少使用一个实例字段。
- 理想情况,算法使用的字段应该不可变(immutable)。
- 算法执行速度尽量快。
- 包含相同值的不同对象应返回相同的hash码。
不要对hash码进行持久化,因为hash算法和hash码很容易被改变。
5. dynamic基元类型
类型安全的编程语言:所有表达式都解析成类型的实例,编译器生成的代码只执行对该类型有效的操作。
与非类型安全的语言相比,类型安全的语言优势:
- 能在编译时检测代码的正确性。
- 编译出更小、更快的代码:能在编译时进行更多预设,并在生成的IL和元数据中落实预设。
dynamic
:
-
为了方便使用反射或者和其他组件进行通信,C#编译器允许将表达式的类型定义为
dynamic
。也可将表达式的结果放入变量,并将变量标记为dynamic
。 -
代码使用
dynamic
表达式/变量调用成员时,编译器生成特殊的IL代码(payload,有效载荷
)来描述所需的操作。 -
在运行时,
payload
代码使用运行时绑定器(runtime binder)
根据dynamic
表达式/变量引用的对象的实际类型来决定具体执行操作。
所有表达式都能隐式转化为dynamic
,dynamic
也可隐式转化为其他类型。
类型和成员基础
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
的非基元类型
的常量变量
。
代码引用常量:
- 编译器在定义常量的程序集的元数据中查找该符号。
- 提取常量值嵌入生成的IL代码中。
- 由于值直接嵌入代码,运行时不需要为常量分配内存。
- 不能获取常量的地址,也不能以传引用的方式传递常量。
2. 字段
字段
是一种数据成员,其中容纳了一个值类型的实例
或对一个引用类型的引用
。字段储存在动态内存中,值在运行时才能获取。
字段修饰符:
CLR术语 | C#术语 | 说明 |
---|---|---|
Static | static | 字段属于类型状态的一部分。 |
Instance | (默认) | 字段属于类型实例状态的一部分。 |
InitOnly | readonly | 1. 字段只能由构造器方法中的代码写入。2. 编译器和验证机制确保readonly字段不会被构造器之外的任何方法写入(可利用反射修改readonly字段)。3. readonly标记的引用类型字段,不可变的是引用,不是引用对象。 |
Volatile | volatile | 编译器、CLR、硬件不会对访问该字段的代码执行“线程不安全”的优化措施。 |
CLR支持:
- 类型(静态)字段:
- 引用类型的方法进入JIT编译时,将类型加载到AppDomain中,创建类型对象,在内存对象中为静态字段分配动态内存。
- 实例(非静态)字段
内联初始化
:在代码中直接赋值来初始化。C#实际是在构造器中对字段进行的初始化,内联初始化是语法上的简化。
方法
1. 实例构造器和类(引用类型)
构造器
:将类型实例初始化为良好状态的特殊方法。构造器在方法定义元数据表
中.ctor
。
创建引用类型实例时:
- 为实例数据字段分配内存
- 初始化对象的附加字段(
overhead fields
, 类型对象指针和同步块索引) - 调用类型的实例构造器设置对象的初始状态
不调用实例构造器,创建类型的实例:
Object > MemberwiseClone()
:作用是分配内存,初始化对象的附加字段,然后将源对象的字节数据复制到新对象中。- 运行时序列化器(runtime serializer)反序列化对象:使用
FormatterServices > GetUninitializedObject()/GetSafeUninitializedObject()
为对象分配内存。
调用构造器:
- 初始化内联初始化的字段
- 调用基类构造器
- 执行构造器自己的代码
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#不允许省略逗号之间的实参。
DefaultParameterValueAttribute
和OptionalAttribute
:
- 在C#中,一旦为参数分配了默认值,编译器就会在内部向该参数应用
OptionalAttribute
特性,该特性会在最终生成的文件的元数据中持久性的存储下来。 - 此外,编译器向参数应用
DefaultParameterValueAttribute
特性,并将该特性持久性的存储到最终生成的文件的元数据中。 - 然后,会向
DefaultParameterValueAttribute
的构造器传递指定的默认值。
2. 隐式类型的局部变量(var)
var
:C#能根据初始化表达式的类型推断方法中的局部变量的类型。
-
只能声明方法内部的局部变量,不能声明类型的字段。
-
不能将
null
赋值给隐式类型的局部变量,因为null
能隐式转换为任何引用类型或可空值类型,编译器无法推断出它的确切类型。 -
不能用
var
声明方法的参数类型。
3. 以引用的方式向方法传递参数(ref、out)
CLR默认所有方法参数都是传值的。CLR允许以传引用而非传值的方式传递参数。
C#使用out
或ref
关键字,编译器将生成代码来传递参数的地址,而非传递参数本身。
CLR不区分out
或ref
关键字,都会生成相同的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预定义的泛型:Action
和Func
委托。
5. 委托和接口的逆变和协变泛型类型实参
委托的每个泛型类型参数都可以标记为协变量
或逆变量
。
泛型类型参数形式:
-
不变量(invariant)
:意味着泛型类型参数不能更改。 -
逆变量(contracariant)
:意味着泛型类型参数可以从一个类
更改为它的某个派生类
,指定参数的兼容性。- C#使用
in
关键字标记逆变量形式的泛型类型参数。 - 逆变量泛型类型参数只能出现在输入位置(方法参数…)。
- C#使用
-
协变量(covariant)
:意味着泛型类型参数可以从一个类
更改为它的某个基类
,指定返回类型的兼容性。- C#使用
out
关键字标记协变量形式的泛型类型参数。 - 协变量泛型类型参数只能出现在输出位置(方法返回值…)。
- C#使用
对于泛型类型参数,如果将该实例的实参传给使用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>>
这样的递归类型。
- 编译器和CLR将
次要约束
类型参数可以指定零个或多个
次要约束。
次要约束:
- 代表接口类型:向编译器承诺:类型参数实现了接口。
- 由于能指定多个接口,所以类型参数必须实现了所有接口约束(以及主要约束)。
- 类型参数约束(裸类型约束):允许一个类型或方法规定:指定的类型实参要么是约束类型,要么是约束类型的派生类。
构造器约束
类型参数可以指定零个或一个
构造器约束。
构造器约束:
-
向编译器承诺:实参是实现了公共无参构造器的非抽象类。
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:接口,能做某事。
- 易用性
- 一致性实现
- 版本控制
「下次一定」
下次一定
使用微信扫描二维码完成支付