第 8 章 方法

第 8 章 方法

本章内容:

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

构造器是将类型的实例初始化为良好状态的特殊方法。创建引用类型的实例时,首先为实例的数据字段分配内存,然后初始化对象的附加字段(类型对象指针和同步块索引),最后调用类型的实例构造器来设置对象的初始状态。

构造引用类型的对象时,在调用类型的实例构造器之前,为对象分配的内存总是先被归零。没有构造器显式重写的所有字段都保证获得 0null值。

实例构造器永远不能被继承。

如果类没有显式定义任何构造器,C#编译器将定义一个默认(无参)构造器。

如果类的修饰符为 abstract,那么编译器生成的默认构造器的可访问性就为 protected;否则,构造器会被赋予 public 可访问性。如果基类没有提供无参构造器,那么派生类必须显式调用一个基类构造器,否则编译器会报错。如果类的修饰符为static(sealedabstract),编译器根本不会在类的定义中生成默认构造器。

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

C#编译器根本不会为值类型内联(嵌入)默认的无参构造器。值类型的实例构造器只有显式调用才会执行。

注意 严格地说,只有当值类型的字段嵌套到引用类型中时,才保证被初始化为 0 或 null。基于栈的值类型字段则无此保证。对于所有基于栈的值类型中的字段,C#和其他能生成“可验证”代码的编译器可以保证对它们进行“置零”,或至少保证在读取之前赋值,确保不会在运行时因验证失败而抛出异常。所以,你完全可以忽略本“注意”的内容,假定自己的值类型的字段都会被初始化为 0 或 null

值类型的任何构造器都必须初始化的全部字段。

8.3 类型构造器

除了实例构造器,CLR 还支持类型构造器(type constructor),也称为静态构造器(static constructor)

类型默认没有定义类型构造器。如果定义,也只能定义一个。此外,类型构造器永远没有参数。类型构造器总是私有的。

类型构造器的调用

IT 编译器在编译一个方法时,会查看代码中都引用了哪些类型。任何一个类型定义了类型构造器,JIT 编译器都会检查针对当前 AppDomain,是否已经执行了这个类型构造器。如果构造器从未执行,JIT 编译器会在它生成的本机(native)代码中添加对类型构造器的调用。如果类型构造器已经执行,JIT 编译器就不添加对它的调用,因为它知道类型已经初始化好了。

CLR 希望确保在每个 AppDomain 中,一个类型构造器只执行一次。为了保证这一点,在调用类型构造器时,调用线程要获取一个互斥线程同步锁。

8.4 操作符重载方法

CLR 规范要求操作符重载方法必须是 publicstatic 方法。另外,C# (以及其他许多语言)要求操作符重载方法至少有一个参数的类型与当前定义这个方法的类型相同。

以下 C# 代码展示了在一个类中定义的操作符重载方法:

1
2
3
public sealed class Complex {
public static Complex operator+(Complex c1, Complex c2) { ... }
}

表 8-1 C# 的一元操作符及其相容于 CLS 的方法名

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; }

表 8-2 C# 的二元操作符及其相容于 CLS 的方法名

C# 操作符 特殊方法名 推荐的相容性于 CLS 的方法名
+ op_Addition Add
- op_Subtraction Subtract
* op_Multiply Multiply
/ op_Division Divide
% op_Modulus Mod
& op_BitwiseAnd BitwiseAnd
` ` op_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

注意 检查 Framework 类库(FCL)的核心数值类型(Int32,Int64UInt32等),会发现它们没有定义任何操作符重载方法。之所以不定义,是因为编译器会(在代码中)专门查找针对这些基元类型执行的操作(运算),并生成直接操作这些类型的实例的 IL指令。如果类型要提供方法,而且编译器要生成代码来调用这些方法,方法调用就会产生额外的运行时开销。另外,方法最终都要执行一些 IL 指令来完成你希望的操作。这正是核心 FCL 类型没有定义任何操作符重载方法的原因。对于开发人员,这意味着假如选择的编程语言不支持其中的某个 FCL 类型,便不能对该类型的实例执行任何操作。

8.5 转换操作符方法

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

以下代码为 Rational 类型添加了 4 个转换操作符方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public sealed class Rational {
// 由一个 Int32 构造一个 Rational
public Rational(Int32 num) { ... }

// 由一个 Single 构造一个 Rational
public Rational(Single num) { ... }

// 将一个 Rational 转换成一个 Int32
public Int32 ToInt32() { ... }

// 将一个 Rational 转换成一个 Single
public Single ToSingle() { ... }

// 由一个 Int32 隐式构造并返回一个 Rational
public static implicit operator Rational(Int32 num) {
return new Rational(num);
}

// 由一个 Single 隐式构造并返回一个 Rational
public static implicit operator Rational(Single num) {
return new Rational(num);
}

// 由一个 Rational 显式返回一个 Int32
public static explicit operator Int32(Rational r) {
return r.ToInt32();
}

// 由一个 Rational 显式返回一个 Single
public static explicit operator Single(Rational r) {
return r.ToSingle();
}
}

8.6 扩展方法

它允许定义一个静态方法,并用实例方法的语法来调用。
现在,当编译器看到以下代码:

1
Int32 index = sb.IndexOf('X');

就首先检查 StringBuilder 类或者它的任何基类是否提供了获取单个 Char 参数、名为 IndexOf 的一个实例方法。如果是,就生成 IL 代码来调用它。如果没有找到匹配的实例方法,就继续检查是否有任何静态类定义了名为 IndexOf 的静态方法,方法的第一个参数的类型和当前用于调用方法的那个表达式的类型匹配,而且该类型必须用 this 关键字标识。

8.6.1 规则和原则

8.6.2 用扩展方法扩展各种类型

由于扩展方法实际是对一个静态方法的调用,所以 CLR 不会生成代码对调用方法的表达式的值进行 null 值检查

8.6.3 ExtensionAttribute 类

在 C# 中,一旦用 this 关键字标记了某个静态方法的第一个参数,编译器就会在内部向该方法应用一个定制特性。该特性会在最终生成的文件的元数据中持久性地存储下来。该特性在 System.Core.dll 程序集中定义,它看起来像下面这样:

1
2
3
// 在 System.Runtime.CompilerServices 命名空间中定义
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class | AttributeTargets.Assembly)]
public sealed class ExtensionAttribute : Attribute { }

8.7 分部方法

规则和原则

关于分部方法,有一些附加的规则和原则需要注意。

  • 它们只能在分部类或结构中声明。

  • 分部方法的返回类型始终是 void,任何参数都不能用out修饰符来标记。之所以有这两个限制,是因为方法在运行时可能不存在,所以不能将变量初始化为方法也许会返回的东西。类似地,不允许 out 参数是因为方法必须初始化它,而方法可能不存在。分部方法可以有 ref 参数,可以是泛型方法,可以是实例或静态方法,而且可标记为unsafe

  • 当然,分部方法的声明和实现必须具有完全一致的签名。如果两者都应了定制特性,编译器会合并两个方法的特性。应用于参数的任何特性也会合并。

  • 如果没有对应的实现部分,便不能在代码中创建一个委托来引用这个分部方法。这同样是由于方法在运行时不存在。编译器报告以下消息:error CS0762:无法通过方法“Base.OnNameChanging(string)”创建委托,因为该方法是没有实现声明的分部方法。

  • 分部方法总是被视为 private 方法,但 C# 编译器禁止在分部方法声明之前添加 private 关键字。