第八章 方法:构造器、操作符、转换操作符和参数
本章内容
实例构造器和类(引用类型)
实例构造器和结构(值类型)
类型构造器
操作符重载
转换操作符方法
通过引用向方法传递参数
向方法传递可变数量的参数
声明方法的参数类型
常量方法和参数
本章将讨论类型中可以定义的各种不同的方法,以及与这些方法相关的各种问题。具体说来,我们将示范如何定义实例和类型的构造器方法、操作符重载方法以及转换操作符方法(包括隐式强制转换和显式强制转换)。另外,本章也涵盖方法的各种参数传递方法,包括:如何通过引用传递参数、如何通过值传递参数和二者的对比以及如何定义可接受可变数量参数的方法。
8.1 实例构造器和类(引用类型)
构造器是允许将类型实例初始化为有效状态的特殊方法。构造器方法在方法定义元数据表(method definition metadata table)中通常称为.ctor(代表构造器)。创建引用类型的实例时,首先为实例的数据字段分配内存,接着初始化对象的系统开销字段(类型对象指针和同步块索引),最后调用类型的实例构造器设置对象的初始状态。
创建引用类型对象时,在调用类型的实例构造器之前,为对象分配的内存始终被清零。构造器没有显式赋值的所有字段保证都有一个0或者null值。
与其他方法不同,实例构造器永远不能被继承,也就是说,类只能拥有类自己定义的实例构造器。因为实例构造器永远不能被继承,所以,下述修饰符不能用于实例构造器:virtual,new,override,sealed和abstract。如果定义的类中没有显式地定义任何构造器,那么,许多编译器(包括C#编译器)将定义一个默认的(无参数的)构造器,该构造器的实现只是调用基类的无参构造器(parameterless constructor)。
例如,对于下面所定义的类:
public class SomeType {
}
上述代码等同于下面的代码:
public class SomeType {
public SomeType() : base() { }
}
|
如果类的修饰符为abstract,那么编译器生成的默认构造器的可访问性为protected;否则,构造器的可访问性为public。如果基类没有提供无参构造器,那么,派生类必须显式地调用基类的构造器,否则编译器会报错。如果类的修饰符为static(sealed和abstract),那么,编译器就根本不会在类的定义中生成一个默认的构造器。
一个类型可以定义多个实例构造器。每个构造器都必须拥有一个不同的签名,而且每个构造器可以拥有不同的可访问性。对于可验证的代码(verifiable code),类的实例构造器在访问从基类继承的任何字段之前,必须调用其基类的构造器。许多编译器,包括C#编译器,会自动地生成对基类构造器的调用,因此一般情况下,根本不必担心或者考虑该问题。最终,类的实例构造器将调用基类System.Object的公有无参构造器。该构造器不执行任何代码,只是简单地返回,因为基类System.Object没有定义实例数据字段,因此它的构造器没有代码可以执行。
在极少数的几种情况下,可以不调用实例构造器来创建类型的实例。例如,调用Object的MemberwiseClone方法时就不需要调用实例构造器,该方法执行以下几步:为对象分配内存、初始化对象的开销字段以及向新对象复制源对象的类型。反序列化(deserializing)对象时,通常也不需要调用构造器。
重要提示 在构造器中不能调用任何虚方法,这样会影响所创建的对象。因为如果重写了要初始化的类型的虚方法,那么派生类型中被重写方法的实现将被执行,但是在该继承层次结构中,并非所有的字段都已被完全初始化。因此在构造器中调用虚方法将导致无法预测的行为。
C#语言提供了一个简单的语法,允许在构建类型实例的过程中初始化引用类型中定义的字段:
Internal sealed class SomeType{
private Int32 m_x = 5;
}
|
构建SomeType对象时,SomeType对象的m_x字段将被初始化为5。这是如何发生的呢?查看一下SomeType的构造器方法(也称作.ctor)的中间语言(Intermediate Language,IL)代码就知道了。IL代码如图8.1所示。
在图8.1中,可以看到SomeType的构造器首先把5存储到m_x字段,接着它又调用基类的构造器。换句话说,C#编译器提供了一个方便的语法来内联初始化实例字段,并且将这个语法转换成构造器方法中的代码以执行初始化。这意味着我们需要注意代码的膨胀效应。如以下类定义所示:
![]() |
| 图8.1 SomeType构造器方法的IL代码 |
internal sealed class SomeType{
private Int32 m_x=5;
private String m_s="Hithere";
private Double m_d=3.14159;
private Byte m_b;
//下面是一些构造器
public SomeType() {...}
public SomeType(Int32x) {...}
public SomeType(Strings){...;m_d=10;}
}
|
编译器为上述三个构造器方法生成代码时,每个方法的入口都包含初始化m_x,m_s和m_d的代码。在这些初始化代码之后,编译器将构造器方法中的代码附加到方法的后面。例如,对于接受String参数的构造器,编译器产生的代码首先是初始化m_x,m_s和m_d,然后才将值10赋值到m_d中。注意,即使没有代码显式初始化m_b,但CLR也要确保m_b被初始化为0。
因为前面提到的类中有三个构造器,所以编译器共产生三次初始化m_x,m_s和m_d的代码——每个构造器一次。如果有几个已初始化的实例字段和许多重载的构造器方法,应考虑在定义字段时避免同时对它们进行初始化,而是将这些公共初始化语句放在一个单独的初始化构造器中,然后让其他构造器显式调用这个公共初始化构造器。这种方法将减少所生成代码的大小。下面的例子演示了如何使用C#的关键字this来显式调用另一个构造器。
Internal sealed class SomeType{
//不要显式初始化下面的字段
private Int32 m_x;
private String m_s;
private Double m_d;
private Byte m_b;
//该构造器将所有的字段都设为默认值。所有其他构造器都必须显式调用这个构造器
public SomeType(){
m_x=5;
m_s="Hi there";
m_d=3.14159;
m_b=0xff;
}
//该构造器首先将所有的字段都设为默认值,然后修改m_x
public SomeType(Int32x):this(){
m_x=x;
}
//该构造器首先将所有的字段都设为默认值,然后修改m_s
public SomeType(Strings):this(){
m_s=s;
}
//该构造器首先将所有的字段都设为默认值,然后修改m_x和m_s
public SomeType(Int32 x,String s):this(){
m_x=x;
m_s=s;
}
}
|
| 回书目 上一节 下一节 |