第九章方法
Ø 9.1实例构造器
如果希望类外的代码创建该类的实例,可以将构造器的访问限制设为私有方式。在创建一个引用类型的实例时,系统会执行以下三个步骤:首先为该实例分配内存,然后初始化对象的附加方法(即方法指针和一个SyncBlockIndex),最后调用类型的实例构造器设置对象的初始状态。
当构造一个引用类型实例时,在调用其实例构造函数之前,系统为该对象分配的内存总是首先被设为0值。
默认情况下,对于引用类型,如果我们没有显式为其定义实例构造器,许多编译器都会为我们定义一个公有的无参构造器,通常称为默认构造器。
一个类的实例构造器在访问其基类的继承字段之前,必须调用其基类的实例构造器。
类型实例的创建不需要调用实例构造器的例子:
1. 调用Object的MemberwiseClone方法将执行以下几步:为对象分配内存,初始化对象的附加成员,将源对象的字节拷贝到新创建的对象中。
2. 反序列化一个对象时,通常也不会调用构造器。
值类型构造器和引用类型构造器的工作方式的差别:
1. 首先CLR没有强制要求值类型中必须定义构造器方法
2. 很多编译器,包括C#,都不会为值类型产生默认的无参构造器。原因是值类型可以被隐式地创建。
3. C#不允许我们为一个值类型定义无参构造器,C#不允许值类型定义无参构造器是为了消除开发人员对于何时调用它们产生的混淆。如果不能定义无参构造器,编译器自然不会产生自动调用它的代码。没有无参构造器,值类型的字段将总是首先被初始化为0或null。
我们可以用另外一种语言,如IL汇编语言,来为值类型定义无参构造器。
为值类型定义的任何构造器都必须要初始化其所有的字段。
不能在值类型中内联实例字段的初始化。
在引用类型的构造器中,this被认为是只读的,所以不能对它进行赋值。
Ø 9.2类型构造器
CLR支持类型构造器(又称静态构造器,类型构造器,或类初始化器)。类型构造器适用于接口(虽然C#不支持),引用类型和值类型。
类型构造器的访问限制应该总为私有方式,这由C#编译器自动来完成。(即为静态构造函数不允许出现访问修饰符)
CLR调用类型构造器的时间:
1. 在类型的第一个实例被创建之前,或者在类型的非继承字段或成员第一次被访问之前。
2. 在非继承静态字段被第一次访问之前的某一时刻。
类型构造器一旦被执行,它在整个应用程序域的生命周期都不会再次被调用。
类型构造器中的代码只能访问类型的静态字段,而且通常它的目的就是初始化这些静态字段。
类型构造器不应该调用其基类型的类型构造器。
Ø 9.3操作符重载方法
访问修饰符必须是public,并且重载的操作符函数必须是静态static的。
Ø 9.4转换操作符方法
和操作符重载方法一样,转换操作符方法必须为public和static。
在C#中我们使用implicit关键字来告诉编译器,在源代码中不必做显示的转型就可以产生调用转换操作符方法的代码;而使用explicit关键字来告诉编译器只用当源代码中指定了显示的转型时,才产生调用转换操作符方法的代码。
在C#中,不能为一种转型操作符同时定义隐式转换操作符和显示转换操作符。
Ø 9.5引用参数
当参数为引用类型的对象时,参数的传递是通过传递指向对象的引用(或指针)来完成的——注意引用/指针本身是按值传递的;对于值类型实例的参数来说,传递给方法的将是值类型实例的一个拷贝。这意味着方法会得到一份属于它自己的值类型实例的拷贝,而调用该方法的代码中的实例不会受到任何影响。
在C#中,可以使用out和ref关键字是参数按引用的方式来传递。这两个关键字告诉C#编译器要产生额外的元数据来表示指定的参数是按引用的方式来传递的;编译器将使用该信息来产生传递参数地址的代码。
Out与ref的不同:如果一个方法的参数被标记为out,那么调用代码在调用该方法之前可以不初始化该参数,并且被调用方法不能直接读取参数的值,它必须在返回之前为该参数赋值。如果一个方法的参数被标记为ref,那么调用代码在调用该方法之前必须首先初始化该参数。被调用方法则可以任意选择读取该字段,或者为该参数赋值。
CLR允许我们根据out和ref参数来重载方法。下面的代码合法:
public class Class1
{
public void Method1(int i){ }
public void Method1(refint i) { }
}
但是仅通过区分out和ref来重载方法又是不合法的,因为它们经过JIT编译后代码是相同的。
public class Class1
{
public void Method1(outint i){ }
public void Method1(refint i) { }
}
在值类型参数上使用out和ref关键字与用传值的方式来传递引用类型的参数在某种程度上具有相同的行为。只有当一个方法要“返回”一个它未知的对象引用时,在引用类型参数上使用out和ref关键字才有意义。Out或ref修饰的引用类型参数的方法创建一个对象后,指向新对象的指针会被返回给调用代码。
区别于委托的协变与抗变,按引用方式传递的变量必须和方法声明的参数类型完全相同。
Ø 9.6可变数目参数
只有方法的最后一个参数才可以用params关键字来标记。该参数必须为一个一维数组,但类型可以任意。传递null或者一个0长数组给该参数是合法的。
编写接受多个任意类型参数的方法:改变方法的原型使其接受一个Object数组即可。
Ø 9.7虚方法的调用机制
一个类型可以有多个同名的方法,只要每个方法的参数集合或者返回值类型互不相同即可。大多数语言在确定方法的唯一性时,都要求名称相同的方法有一个不同的参数集合,而忽略返回值的差别。
所有的实例方法都会接受一个隐藏的this指针作为方法的第一个参数。其中this指针指向当前正在操作的对象。
Ø 9.8虚方法的版本问题
使用new关键字可以隐藏方法的基类版本。
Ø 8.6 扩展方法
编译器首先检查调用方法的类或者它的任何基类是否提供了相同参数签名的方法。如果存在这样的一个实例方法,编译器会生成IL代码来调用它。如果没有发现匹配的实例方法,则继续检查是否有任何静态类定义了一个相同名字的静态方法,它的第一个参数是和当前用于调用方法的那个类型匹配的一个类型,而且这个类型必须用this关键字标识。
8.6.1 规则和原则
扩展方法必须在非泛型的静态类中声明。当然,扩展方法至少要有一个参数,而且只有第一个参数能用this关键字标记。
C#编译器查找静态类中定义的扩展方法时,要求这些静态类本身必须具有文件作用域。扩展方法必须在顶级静态类中定义,而不是嵌套类。
为增强性能,并避免找到非你所愿的一个扩展方法,C#编译器要求“导入”扩展方法。
8.6.2 用扩展方法扩展各种类型
可以为接口类型扩展方法,还可以为委托类型定义扩展方法,或者为枚举类型扩展方法。
8.6.3 ExtensionAttribute类
在C#中,一旦用this关键字标记了某个静态方法的第一个参数,编译器就会在内部向该方法应用一个定制attribute——ExtensionAttribute。
如果代码调用了一个不存在的实例方法,编译器就会快速扫描引用的所有程序集,判断它们哪些包含了扩展方法。然后在这些程序集中,可以只扫描包含了扩展方法的静态类。在每个这样的静态类中,可以只扫描扩展方法来查找匹配。
Ø 8.7 分部方法
包含分布方法的类可以是密封的,也可以是静态类,甚至可以是值类型。
工具生成的代码包含分布方法的声明。要用partial关键字标记,没有实现。
开发者生成的代码实现这个声明。该方法也要用partial关键字标记,有实现。
如果没有实现分布方法,编译器不会生成任何代表分布方法的元数据。
sealed partial class Class1
{
int value;
partial void Method1(Class1 c1);
}
partial class Class1
{
partial void Method1(Class1 c1)
{
this.value = c1.value;
}
}
规则和原则
它们只能在分布类或者结构中声明。
分布方法的返回类型始终是void,任何参数都不能用out修饰符来标记。之所以有这两个限制,是因为方法在运行时可能不存在,所以不能将一个变量初始化为方法也许会返回的东西。类似地,之所以不能有out参数,是因为方法必须初始化它,而这个方法可能是不存在的。分布方法可以有ref关键字,可以是泛型方法,可以是实例或静态方法,而且可以标记为unsafe。
当然,分布方法的声明和实现必须具有完全一致的签名。如果两者都应用了定制attribute,编译器会将这两个方法的attribute合并到一起。应用于参数的任何attribute也会合并。
分布方法总是被视为private方法。但是,C#编译器禁止你在分布方法声明之前添加private。