第9章 方法 (1)
9.1 实例构造器
CLR要求每个引用类型至少定义一个实例构造器。在创建一个引用类型的实例时,系统首先为该实例分配内存,然后初始化对象的附加成员(即方法表指针和一个SyncBlockIndex),最后调用类型的实例构造器设置对象的初始化状态
对于引用类型,默认情况下如果没有显式定义实例构造器,许多编译器包括C#都会自动定义一个公有的无参构造器
少数几种情况下,类实例的创建不需要调用实例构造器,如调用Object的MemberwiseClone方法。另外反序列化一个对象时通常也不会调用构造器。(什么是反序列化?)
C#中用内联语法来初始化字段时,观察IL代码,可以看到构造器首先把值赋给字段,之后又调用了基类的构造器。这样当构造器方法较多时,每一个方法的开始处都将包括初始化这些字段的代码。如果我们有一些需要初始化的实例字段和许多重载的构造器方法,我们应该考虑在定义字段的时候避免同时对它们进行初始化,而应该将这些公共的初始化语句放在一个初始化构造器中,然后使其他的构造器显式的调用这个初始化构造器,这样将有助于减少生成代码的尺寸
对于值类型,CLR不要求它必须有构造器方法,且很多编译器包括C#都不会为值类型产生默认的无参构造器,但CLR还是允许我们为值类型定义构造器,构造器的作用只是用来初始化已分配好内存的值类型
值类型的构造器必须要在被显式调用的时候才会执行,为了防止混淆,C#不允许值类型定义无参数的构造器,不允许字段内联初始化,也不允许构造器没有初始化所有字段
9.2 类型构造器
CLR中类型构造器适用于接口、引用类型和值类型,然而C#不支持接口的类型构造器。类型没有默认的类型构造器,要定义也只能定义一个,且构造器不能带参数
C#中类型构造器的访问限制总是为私有,不能在程序中设置访问修饰符。
CLR会选择下列时间之一来调用类型构造器:
1、在类型的第一个实例被创建之前,或者在类型的非继承字段或成员第一次被访问之前(just before)。因为CLR会在确定的时刻调用类型构造器,故这被称作精确语义(precise semantics)
2、在非继承静态字段被第一次访问之前的某个时刻。因为CLR只保证静态构造器总是在静态字段被访问前的某个时刻运行,故这被称作字段初始化前语义(befor-field-init semantics)
默认情况下编译器选择对我们定义的类型最有意义的语义,并通过设置beforefieldinit元数据标记来告诉CLR这种选择。类型构造器一旦被执行,在整个应用程序域(AppDomain)的生命周期内都不会再次被调用。因为是CLR在负责调用类型构造器,调用的时间不能确定,所以我们应该避免编写需要以特殊顺序调用类型构造器的代码
CLR只保证类型构造器开始执行而不保证完成执行,这样可以避免两个类型构造器互相引用时出现死锁的情况
如果一个类型构造器抛出一个未处理的异常,CLR将认为该类型不可用,试图访问其的任何字段或方法都会抛出System.TypeInitializationException异常
类型构造器不应该调用其基类型的类型构造器,因为基类型中的静态字段并没有被派生类所继承,派生类内部可以引用基类型的静态字段或者可以通过派生类来引用基类的静态字段并不是通过继承,而是编译时静态绑定。如果希望在调用类型构造器时同时调用所有基类和实现接口中的类型构造器(如JAVA),CLR通过System.Runtime.CompilerServices.RuntimeHelpers类型的RunClassComstructor方法为编译器和开发人员提供了这种语义
只有当应用程序域关闭时类型才会被卸载。CLR不支持静态的Finalize方法,如果希望应用程序域关闭时能执行某些代码,可以在System.AppDomain类型的DomainUnload事件上登记一个回调方法
9.3 操作符重载方法
CLR对操作符重载一无所知,甚至都不认识操作符是什么,是具体的编程语言定义了每个操作符的含义。但CLR却规范了编程语言应该怎样提供操作符重载
C#中操作符重载方法必须为public和static,并使用operator关键字
在C#中,假如我们重载了加号,编译器会产生一个名为op_Addition的方法定义,该方法定义条目上有一个specialname标记,表示是一个“特殊”的方法。当编译器看到源代码中的加号操作符时,会查看其中的操作符类型中有哪一个定义了参数类型和操作数类型兼容而名为op_Addition的specialname方法并调用
一些FCL类型如Int32中没有定义任何的操作符重载方法,因为CLR提供了直接操作这些类型实例的IL指令
9.3.1 操作符与语言互操作性
并不是所有的编程语言都支持操作符重载,如VB和Java。VB不知道怎样将使用+操作符的代码翻译成用op_Addition方法的代码,但可以调用由C#编译器生成的类型中的op_Addition方法
然而如果VB中定义一个op_Addition方法,在C#中仍然不可以用+操作符来调用,因为C#编译器查找的操作符重载方法是带有specialname元数据标记的op_Addition方法,只能通过显式调用这个op_Addition方法来执行其中的操作
而在C#定义的操作符重载则可以在VB中通过显示调用op_Addition方法来执行,即使该方法有一个specialname元数据标记(C#中可不可以直接定义一个op_Addtion方法?如果C#的一个类中既定义了加号操作符重载又定义了op_Addtion方法,那么VB调用时会采用哪个?)
微软建议我们在定义操作符重载的类型中同时定义一个命名友好的公有实例方法,再在该方法内部调用操作符重载方法。例如我们应该在定义了加号操作符重载方法的类型中再定义一个公有的Add方法,方便其他不支持操作符重载的语言调用。对每一个C#中的操作符,微软都给出了推荐的与CLR兼容的友好方法名
9.4 转换操作符方法
假设FCL中包括一个Rational类型,为了将一个类型如Int32转换为一个Rational,以及将Rational转换为一个Int32,一种方法是可以在Rational类型中定义一些公有的构造器,其参数应该为我们希望转换的类型实例如Int32的实例,并且为Rational类型定义一些不接受任何参数的公友实例方法ToXXX(类似ToString),这样使用任何编程语言的开发者都可以通过调用这些构造器和方法来实现Int32和Rational的互换
转换操作符可以将对象从一个类型转换为另一个类型,一些编程语言如C#支持转换操作符重载
C#中转换操作符方法必须为public和static,还必须使用implicit或explicit来告诉编译器是隐式的产生代码来调用转换操作符方法还是必须通过源代码来显式的告诉编译器,之后还需要指定operator关键字来告诉编译器该方法是一个转换操作符。下面的例子可以看出操作符重载和转换操作符重载语法上的一些不同:
public static Int32 operator + (Int32 x, Int32 y) { ... }
public static explicit operator Int32(Rational r) { ... }
public static implicit operator Rational(Int32 numberator) { ... }
(Rational类型里定义的转换操作符重载方法是否必须要跟Rational有关,如由某种类型的对象转变为Rational对象或由Rational对象转换为其他类型对象?还是说可以在Rational类型里定义任意对象间的转换操作符方法而至少编译时不会报错?)
C#编译器为每个转换操作符产生了一个方法,名称总是为op_Implicit或op_Explicit,只是参数或返回值类型不同。事实上,CLR完全支持一个类型定义多个只有返回值类型有差别的方法,只是很少的语言提供了这种能力。C#编译器虽然没有为开发人员提供这种能力,但在使用转换操作符方法时却可以在内部利用这样的机制区分只有返回值类型不同的方法