第八章 方法
第一节 实例构造器和类(引用类型)
1、构造器方法在“方法定义元数据表”中始终叫做.ctor(constructor的简称)。创建引用类型实例时,首先为实例的数据字段分配内存,然后初始化对象的附加字段(类型对象指针和同步块索引),最后调用类型的实例构造器来设置对象的初始状态。
2、构造引用类型的对象时,在调用类型的实例构造器之前,为对象分配的内存总是先被归零。
3、实例构造器永远不能被继承。所以实例构造器不能使用以下修饰符:virtual、new、override、sealed、abstract。
4、如果类没有显式定义任何构造器,则编译器会定义一个默认的无参构造器,它的实现只是简单地调用了基类的无参构造器。
5、如果类的修饰符为abstract,那么编译器生成的默认构造器的可访问性就为protected;否则,构造器会被赋予public可访问性。如果基类没有提供无参构造器,那么派生类必须显式调用一个基类构造器,否则编译会报错。如果类的修饰符是static(静态类在元数据中是抽象密封类sealed和abstract的 )编译器不会在类的定义中生成默认构造器。
6、类的实例构造器在访问从基类继承的任何字段之前,必须先调用基类的构造器。如果派生类的构造器没有显式调用一个基类,C#编译器会自动生成队默认的基类构造器的调用。所以,所有的类在构造的时候都会调用Object的公共无参构造器。
提示:创建类的实例的时候并不是总要调用实例构造器。一个典型的例子是Object的MemoberwiseClone方法,其作用是分配内存,始化附加字段,并且将源对象的字节数据复制到新的对象中。另外,用运行时序列化器反序列化对象时,通常也不需要调用构造器。
7、内联初始化。内联初始化是一种简化的初始化方法。C#编译器会将内联初始化的代码转换成构造器方法中的代码来执行初始化,正因为如此,我们应该注意代码的膨胀效应。
internal sealed class SomeType {
private Int32 m_x = 5;
private String m_s = "Hi there";
private Double m_d = 3.14159;
private Byte m_b;
//下面三个构造器方法都会在开始的位置生成m_x,m_s,m_d的初始化代码,造成代码膨胀
public SomeType(){...}
public SomeType(Int32 x){...}
public SomeType(String s){...;m_d=10;}
}
8、构造器方法的顺序是:①自动生成内联初始化的代码 ②调用基类的构造器 ③执行自己的代码。(再体会P164页的例)
提示:之所以在调用基类的构造器之前使用简化语法对所有字段进行初始化,是为了维持源代码给人留下的“这些字段总是有一个值”的印象。如果字段没有初始化,假如基类构造器调用了一个虚函数回调派生类定义的方法可能会出现问题。
第二节 实例构造器和结构(值类型)
1、值类型构造器的工作方式与引用类型完全不同。CLR永远允许创建值类型的实例,并且没有办法阻止值类型的实例化。所以值类型其实不需要定义构造器,C#编译器不会为值类型内联无参构造器。所以值类型不能使用内联初始化。
2、CLR不会为引用类型中的每个值类型字段都调用构造器。但是,值类型的字段会被初始化为0或null。
3、CLR确实允许为值类型定义构造器。但必须显式调用才会执行。如下例:
internal struct Point {
public Int32 m_x,m_y;
public Point(){
m_x = m_y = 5 ;
}
}
internal sealed class Rectangle {
public Point m_topLeft,m_bottomRight;
public Rectangle(){}
}
//注意即使Point中定义了无参构造器,但是为了提升应用程序的性能,C#不会生成代码来自动调用它
//如果想要执行无参构造器,开发人员必须增加显式调用值类型构造器的代码
//所以Rectangle类的两个Point字段会被初始化为0
//实际上这段代码是编译不了的:error CS0568:结构不能包含显式的无参数构造器
4、为了生成“可验证”的代码,在访问值类型的任何字段之前,修需要对全部字段进行赋值。换言之,值类型的任何构造器都必须初始化值类型的全部字段。例:
internal struct SomeValType {
private Int32 m_x,m_y;
//这个构造器不能通过编译,因为没有对m_y进行初始化
public SomeValType(Int32 x){
m_x = x;
}
//这个构造器看起来很奇怪,但是可以通过编译,因为已经对所有字段进行了初始化
public SomeValType(Int32 x){
this = new SomeValType();
m_x = x;
}
}
第三节 类型构造器
1、类型构造器可以用于接口(C#编译器不允许)、引用类型和值类型。
2、类型默认没有定义类型构造器。如果定义也只能定义一个。此外,类型构造器永远没有参数。
internal sealed class SomeRefType {
static SomeRefType(){
//SomeRefType被首次访问时,执行这里的代码
}
}
internal struct SomeValType {
static SomeValType(){
//SomeValType被首次访问时,执行这里的代码
}
}
3、类型构造器必须为static。此外,类型构造器总是私有的;C#自动把它们标记为private。事实上,如果在源代码中显式的将类型构造器标记为private(或其他访问修饰符),C#编译器会显示error CS0515:静态构造函数中不允许出现访问修饰符。之所以必须私有,是为了防止任何由开发人员写的代码调用它,对它的调用总是由CLR负责。
4、类型构造器的调用细节:P167
5、值类型可以定义类型构造器,但是最好不要这么做,因为CLR有时候不会调用值类型的静态构造器。例见P168。
6、类型构造器中的代码只能访问类型的静态字段,并且它的常规用途就是初始化这些字段。静态字段也可以使用内联初的方法进行初始化。
提示:虽然之前说C#不允许对值类型的实例字段进行内联初始化,但是可以对值类型的静态字段使用内联初始化。
7、类型构造器不应调用基类型的类型构造器。这种调用之所以没必要,是因为类型不可能有静态字段是从基类型分享或继承的。 (原因?)
提示:有的语言(比如Java)希望在访问类型是自动调用它的类型构造器,并调用它的所有基类型的类型构造器。此外,类型实现的接口也必须调用接口的类型构造器。CLR不支持这种行为。
第四节 操作符重载方法
1、对编程语言的选择决定了你是否获得对操作符重载的支持,以及具体的语法是什么。CLR规范要求操作符重载方法必须是public和static的方法。另外,C#要求操作符重载方法至少有一个参数与当前定义这个方法的类型相同。之所以有这个的限制,是为了使C#编译器能在合理的时间内找到要绑定的操作符方法。以下代码演示了如何在C#中进行操作符重载。
public sealed class Complex {
public static Complex operator+(Complex c1, Complex c2){..}
}
2、在上述代码中,编译器为名为op_Addition的方法生成元数据方法定义项;这个方法定义项还设置了specialname标签,表明这是一个特殊方法。编译器看到源代码中出现一个+操作符的时候,会检查是否有一个操作数的类型定义了名为op_Addition的specialname方法,而且该方法的参数兼容于操作数的类型。如果存在这样的方法,编译器就生成调用它的代码,否则就报告编译错误。
第五节 转换操作符方法
1、有的编程语言(如C#)提供了转换操作符重载。转换操作符是将对象从一种类型转换成另一种类型的方法。
2、如果需要定义一个Rational(有理数)类型,支持Int32或者Single转换为Rational(包括还原)的代码如下:
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(){..}
}
3、有些编程语言提供了转换操作符重载。转换操作符是将对象从一种类型转换为另一种类型的方法。CLR规范要求转换操作符重载方法必须是public和static的方法。此外C#要求参数类型和返回类型二者必有其一与定义转换方法的类型相同。(原因是为了使得C#编译器能在一个合理的时间内找到要绑定的操作符方法)
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();
}
//由一个Tational显式返回一个Single
public static explicit operator Single(Rational r){
return r.ToSingle();
}
}
4、在C#中,implicit关键字告诉编译器为了生成代码来调用方法,不需要在源代码中进行显式转换。相反,explicit关键字告诉编译器只有在发现了显式转型时,才调用方法。在implicit或者explicit关键字之后,要制定operator关键字告诉编译器该方法是一个转换操作符。在operator之后,指定对象要转换成什么类型。在圆括号内,则指定要从什么类型转换。
public sealed class Program{
public static void Main() {
Rational r1 = 2; //Int32隐式转换为Rotional
Rational r2 = 2.5F; //Single隐式转换为Rational
Int32 x = (Int32) r1; //Rational显式转型为Int32
Single s = (Single) r2; //Rational显式转型为Single
}
}
5、Rotional类型的4个转换操作符方法的元数据定义如下,从中可以看出类型转换方法总是叫做op_Implicit或者op_Explicit。只有在转换不损失精度或者数量级的前提下才能定义隐式转换操作符。如果转换会造成精度或者数量级的损失,就应该定义一个显式转换操作符。显式转换失败,应该让显式转换操作符方法抛出异常。
public static Rational op_Implicit(Int32 num)
public static Rational op_Implicit(Single num)
public static Int32 op_Explicit(Rational r)
public static Single op_Explicit(Rational r)
提示:仔细观察上述代码可以看出,op_Explicit方法获取相同的参数,仅仅返回值不同。这是仅凭方法返回值来区分方法的例子。CLR允许在一个类型中定义仅返回类型不同的多个方法,但是大部分语言不支持这个能力。C#没有向程序员开放这个能力,但是在其内部其实是会利用这个能力的。(个人猜想:可能因为这个功能与var的功能冲突)
提示:使用强制类型转换表达式时,C#生成代码来调用显式转换操作符方法。使用C#的is或者as操作符时,则永远不会调用这些方法。
第六节 扩展方法
1、