C# 继承细节


  假定没有为类定义任何显式的构造函数,这样编译器就会为所有的类提供默认的构造函数,在后
台会进行许多操作,编译器可以很好地解决层次结构中的所有问题,每个类中的每个字段都会初始化
为默认值。但在添加了一个我们自己的构造函数后,就要通过派生类的层次结构高效地控制构造过程,
因此必须确保构造过程顺利进行,不要出现不能按照层次结构进行构造的问题。
  为什么派生类会有某些特殊的问题?原因是在创建派生类的实例时,实际上会有多个构造函数起
作用。要实例化的类的构造函数本身不能初始化类,还必须调用基类中的构造函数。这就是为什么要
通过层次结构进行构造的原因。
  为了说明为什么必须调用基类的构造函数,下面是手机公司MortimerPhones 开发的一个例子。这
个例子包含一个抽象类GenericCustomer,它表示顾客。还有一个(非抽象)类Nevermore60Customer,
它表示采用特定付费方式(称为Nevermore60 付费方式)的顾客。所有的顾客都有一个名字,由一个私
有字段表示。在Nevermore60 付费方式中,顾客前几分钟的电话费比较高,需要一个字段
highCostMinutesUsed,它详细说明了每个顾客该如何支付这些较高的电话费。抽象类GenericCustomer
的定义如下所示:
abstract class GenericCustomer
{
private string name;
// lots of other methods etc.
}
class Nevermore60Customer : GenericCustomer
{
private uint highCostMinutesUsed;
// other methods etc.
}
  不要担心在这些类中执行的其他方法,因为这里仅考虑构造过程。如果下载了本章的示例代码,
就会发现类的定义仅包含构造函数。
  下面看看使用new 运算符实例化Nevermore60Customer 时,会发生什么情况:
GenericCustomer customer = new Nevermore60Customer();
98 / 826
  显然,成员字段name 和highCostMinutesUsed 都必须在实例化customer 时进行初始化。如果没
有提供自己的构造函数, 而是仅依赖默认的构造函数, name 就会初始化为null 引用,
highCostMinutesUsed 初始化为0。下面详细讨论其过程。
highCostMinutesUsed 字段没有问题:编译器提供的默认Nevermore60Customer 构造函数会把它初
始化为0。
  那么name 呢?看看类定义,显然,Nevermore60Customer 构造函数不能初始化这个值。字段name
声明为private,这意味着派生的类不能访问它。默认的Nevermore60Customer 构造函数甚至不知道存
在这个字段。唯一知道这个字段的是GenericCustomer 的其他成员,即如果对name 进行初始化,就必
须在GenericCustomer 的某个构造函数中进行。无论类层次结构有多大,这种情况都会一直延续到最
终的基类System.Object 上。
  理解了上面的问题后,就可以明白实例化派生类时会发生什么样的情况了。假定默认的构造函数
在整个层次结构中使用: 编译器首先找到它试图实例化的类的构造函数, 在本例中是
Nevermore60Customer,这个默认Nevermore60Customer 构造函数首先要做的是为其直接基类
GenericCustomer 运行默认构造函数,然后GenericCustomer 构造函数为其直接基类System.Object 运行
默认构造函数,System. Object 没有任何基类,所以它的构造函数就执行,并把控制权返回给
GenericCustomer 构造函数。现在执行GenericCustomer 构造函数,把name 初始化为null,再把控制
权返回给Nevermore60Customer 构造函数,接着执行这个构造函数,把highCostMinutesUsed 初始化
为0,并退出。此时,Nevermore60Customer 实例就已经成功地构造和初始化了。
构造函数的调用顺序是先调用System.Object,再按照层次结构由上向下进行,直到到达编译器要
实例化的类为止。还要注意在这个过程中,每个构造函数都初始化它自己的类中的字段。这是它的一
般工作方式,在开始添加自己的构造函数时,也应尽可能遵循这个规则。
  注意构造函数的执行顺序。基类的构造函数总是最先调用。也就是说,派生类的构造函数可以在
执行过程中调用它可以访问的基类方法、属性和其他成员,因为基类已经构造出来了,其字段也初始
化了。如果派生类不喜欢初始化基类的方式,但要访问数据,就可以改变数据的初始值,但是,好的
编程方式应尽可能避免这种情况,让基类构造函数来处理其字段。
理解了构造过程后,就可以开始添加自己的构造函数了。
1. 在层次结构中添加无参数的构造函数
首先讨论最简单的情况,在层次结构中用一个无参数的构造函数来替换默认的构造函数后,看看
会发生什么情况。假定要把每个人的名字初始化为<no name>,而不是null 引用,修改GenericCustomer
中的代码,如下所示:
public abstract class GenericCustomer
{
private string name;
public GenericCustomer()
: base() // we could omit this line without affecting the compiled
code
{
name = "<no name>";
}

添加这段代码后,代码运行正常。Nevermore60Customer 仍有自己的默认构造函数,所以上面描
述的事件顺序仍不变,但编译器会使用定制的GenericCustomer 构造函数,而不是生成默认的构造函
数,所以name 字段按照需要总是初始化为<no name>。
注意,在定制的构造函数中,在执行GenericCustomer 构造函数前,添加了一个对基类构造函数
的调用,使用的语法与前面解释如何让构造函数的不同重载版本互相调用时使用的语法相同。唯一的
区别是,这次使用的关键字是base,而不是this,表示这是基类的构造函数,而不是要调用的类的构
99 / 826
造函数。在base 关键字后面的圆括号中没有参数,这是非常重要的,因为没有给基类构造函数传送
参数,所以编译器会调用无参数的构造函数。其结果是编译器会插入调用System.Object 构造函数的
代码,这正好与默认情况相同。
实际上,可以把这行代码删除,只加上为本章中大多数构造函数编写的代码:
public GenericCustomer()
{
name = "<no name>";
}
如果编译器没有在起始花括号的前面找到对另一个构造函数的任何引用,它就会假定我们要调用
基类构造函数--这符合默认构造函数的工作方式。
base 和 this 关键字是调用另一个构造函数时允许使用的唯一关键字,其他关键字都会产生编译
错误。还要注意只能指定一个其他的构造函数。
到目前为止,这段代码运行正常。但是,要通过构造函数的层次结构把级数弄乱的最好方法是把
构造函数声明为私有:
private GenericCustomer()
{
name = "<no name>";
}
如果试图这样做,就会产生一个有趣的编译错误,如果不理解构造是如何按照层次结构由上而下
的顺序工作的,这个错误会让人摸不着头脑。
'Wrox.ProCSharp.GenericCustomer()' is inaccessible due to its
protection level
有趣的是,该错误没有发生在GenericCustomer 类中,而是发生在Nevermore60Customer 派生类
中。编译器试图为Nevermore60Customer 生成默认的构造函数,但又做不到,因为默认的构造函数应
调用无参数的GenericCustomer 构造函数。把该构造函数声明为private,它就不可能访问派生类了。
如果为GenericCustomer 提供一个带有参数的构造函数,但没有提供无参数的构造函数,也会发生类
似的错误。在本例中,编译器不能为GenericCustomer 生成默认构造函数,所以当编译器试图为派生
类生成默认构造函数时,会再次发现它不能做到这一点,因为没有无参数的基类构造函数可调用。这
个问题的解决方法是为派生类添加自己的构造函数-- 实际上不需要在这些构造函数中做任何工作,这
样,编译器就不会为这些派生类生成默认构造函数了。

原文地址:https://www.cnblogs.com/opps/p/3727599.html