在 C#中定义类时,常常不需要定义相关的构造函数和析构函数,因为在编写代码时,如果没有提供它们,编译器会自动添加它们。但是,如有必要,可以提供自己的构造函数和析构函数,以便初始化对象和清理对象。
使用下述语法可以把一个简单的构造函数添加到类中:
class MyClass { public MyClass() { // Constructor code. } }
这个构造函数与包含它的类同名,且没有参数(使其成为类的默认构造函数),这是一个公共函数,所以类的对象可以使用这个构造函数进行实例化(详见第 8 章)。
class MyClass { private MyClass() { // Constructor code. } }
最后,通过提供参数,也可以采用相同的方式给类添加非默认的构造函数,例如:
class MyClass { public MyClass() { // Default constructor code. } public MyClass(int myInt) { // Nondefault constuctor code (uses myInt). } }
可提供的构造函数的数量不受限制(当然不能耗尽内存,也不能有相同的参数集,所以 “几乎不受限制” 更合适)。
使用略微不同的语法来声明析构函数。在.NET 中使用的析构函数(由 System.Object 类提供)称为 Finalize()
,但这不是我们用于声明析构函数的名称。使用下面的代码,而不是重写 Finalize()
:
class MyClass { ~MyClass() { // Destructor body. } }
类的析构函数由带有 ~
前缀的类名来声明(构造函数也使用类名声明)。当进行垃圾回收时,就执行析构函数中的代码,释放资源。在调用这个析构函数后,还将隐式地调用基类的析构函数,包括 System.Object 根类中的 Finalize()
调用。该技术可以让.NET Framework 确保调用 Finalize()
,因为重写 Finalize()
是指基类调用需要显式地执行,这是具有潜在危险的(第 10 章 将详细讨论如何调用基类的方法)。
构造函数的执行序列
如果在类的构造函数中执行多个任务,把这些代码放在一个地方是非常方便的,这与第 6 章论述的把代码放在函数中有相同的优势。使用一个方法就可以把代码放在一个地方(详见第 10 章),但 C#提供了一个更好的方式。任何构造函数都可以配置为在执行自己的代码前调用其他构造函数。
在讨论构造函数前,先看一下在默认情况下,创建类的实例时会发生什么情况。出了前面说过的便于把初始化代码集中起来之外,还要了解这些代码。在开发过程中,由于调用构造函数时出现错误,对象常常并没有按照预期的那样执行。发生构造函数调用错误常常是因为类继承结构中的某个基类没有正确实例化,或者没有正确地给基类构造函数提供信息。如果理解在对象生命周期的这个阶段发生事情,将更利于解决此类问题。
为了实例化派生的类,必须实例化它的基类。而要实例化这个基类,又必须实例化这个基类的基类,这样一直到实例化 System.Object(所有类的根)为止。结果是无论使用什么构造函数实例化一个类,总是首先调用 System.Object.Object()
。
无论在派生类上使用什么构造函数(默认的构造函数或非默认的狗杂函数),除非明确指定,否则就使用基类的默认构造函数(稍后将介绍如何改变这个行为)。下面介绍一个简短示例,说明执行的顺序。考虑下面的对象层次结构:
public class MyBaseClass { public MyBaseClass() { } public MyBaseClass(int i) { } } public class MyDerivedClass : MyBaseClass { public MyDerivedClass() { } public MyDerivedClass(int i) { } public MyDerivedClass(int i, int j) { } }
如果以下面的方式实例化 MyDerivedClass
:
MyDerivedClass myObj = new MyDerivedClass();
则执行顺序如下:
● 执行 System.Object.Object()构造函数。
● 执行 MyBaseClass.MyBaseClass()构造函数。
● 执行 MyDerivedClass.MyDerivedClass()构造函数。
另外,如果使用下面的语句:
MyDerivedClass myObj = new MyDerivedClass(4);
则执行顺序如下:
● 执行 System.Object.Object()构造函数。
● 执行 MyBaseClass.MyBaseClass()构造函数。
● 执行 MyDerivedClass.MyDerivedClass(int i)构造函数。
最后,如果使用下面的语句:
MyDerivedClass myObj = new MyDerivedClass(4, 8);
则执行顺序如下:
● 执行 System.Object.Object()构造函数。
● 执行 MyBaseClass.MyBaseClass()构造函数。
● 执行 MyDerivedClass.MyDerivedClass(int i, int j)构造函数。
大多数情况下,这个系统都能正常工作。但是,有时需要对发生的事情进行更多的控制。例如,在上面的实例化示例中,可能想得到如下所示的执行顺序:
● 执行 System.Object.Object()构造函数。
● 执行 MyBaseClass.MyBaseClass(int i)构造函数。
● 执行 MyDerivedClass.MyDerivedClass(int i, int j)构造函数。
使用这个顺序,可以把使用 int i
参数的代码放在 MyBaseClass(int i)
中,即 MyDerivedClass(int i, int j)
构造函数要做的工作比较少,只需要处理 int j
参数(假定 int i
参数在两种情况下含义相同,虽然事情并非总是如此,但实际上我们常常做这样的安排)。只要愿意,C#就可以指定这种操作。
为此,只需使用构造函数初始化器,它把代码放在方法定义的冒号后面。例如,可以在派生类的构造函数定义中指定所使用的基类构造函数,如下所示:
public class MyDerivedClass : MyBaseClass { ... public MyDerivedClass(int i, int j) : base(i) { } }
其中,base
关键字指定.NET 实例化过程使用基类中具有指定参数的构造函数。这里使用了一个 int
参数(其值通过参数 i
传送给 MyDerivedClass
构造函数),所以将使用 MyBaseClass(int i)
。这么做将不会调用 MyBaseClass()
,而是执行本例前面列出的事件序列--也就是我们希望执行的事件序列。
也可以使用这个关键字指定基类构造函数的字面值,例如,使用 MyDerivedClass
的默认构造函数来调用 MyBaseClass
的非默认构造函数:
public class MyDerivedClass : MyBaseClass { public MyDerivedClass() : base(5) { } ... }
这段代码将执行下述序列:
● 执行 System.Object.Object() 构造函数。
● 执行 MyBaseClass.MyBaseClass(int i) 构造函数。
● 执行 MyDerivedClass.MyDerivedClass() 构造函数。
除了 base
关键字外,这里还可将另一个关键字 this
用作构造函数初始化器。这个关键字指定在调用指定的构造函数前,.NET 实例化过程对当前类使用非默认的构造函数。例如:
public class MyDerivedClass : MyBaseClass { public MyDerivedClass() : this(5, 6) { } ... public MyDerivedClass(int i, int j) : base(i) { } }
使用 MyDerivedClass.MyDerivedClass
构造函数,将执行下述序列:
● 执行 System.Object.Object() 构造函数。
● 执行 MyBaseClass.MyBaseClass(int i) 构造函数。
● 执行 MyDerivedClass.MyDerivedClass(int i, int j) 构造函数。
● 执行 MyDerivedClass.MyDerivedClass() 构造函数。
● 唯一的限制是使用构造函数初始化器只能指定一个构造函数。但如上一个示例所示,这并不是一个很严格的限制,因为我们仍可以构造相当复杂的执行序列。
如果没有给构造函数指定构造函数初始化器,编译器就会自动添加
base()
。这会执行本节前面介绍的默认序列。
注意在定义构造函数时,不要创建无限循环。例如:
public class MyBaseClass { public MyBaseClass() : this(5) { } public MyBaseClass(int i) : this() { } }
使用上述任何一个构造函数,都需要首先执行另一个构造函数,而另一个构造函数要求首先执行原构造函数。这段代码可以编译,但如果尝试实例化 MyBaseClass
,就会得到一个 SystemOverflowException
异常。