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 生成默认构造函数,所以当编译器试图为派生
类生成默认构造函数时,会再次发现它不能做到这一点,因为没有无参数的基类构造函数可调用。这
个问题的解决方法是为派生类添加自己的构造函数-- 实际上不需要在这些构造函数中做任何工作,这
样,编译器就不会为这些派生类生成默认构造函数了。
C# 继承细节的更多相关文章
- python继承细节
不要子类化内置类型 内置类型(由C语言编写)不会调用用户定义的类覆盖的特殊方法. 例如,子类化dict作为测验: class DoppeDict(dict): def __setitem__(self ...
- C++继承细节 -2
继承与动态内存分配 //基类定义 class BaseClass { private: char *label; public: BaseClass() {} BaseClass(const char ...
- C++继承细节 -1
为什么基类析构函数最好要使用 virtual 进行修饰? class A { private: ...... public: ~A(); A() {} }; class B : public A { ...
- JAVA_SE基础——39.继承
在面向对象程序设计中,可以从已有的类派生出新类. 这称做继承(inheritance). 白话解释: 例子1:继承一般是指晚辈从父辈那里继承财产,也可以说是子女拥有父母给予他们的东西. 例子2:猫和狗 ...
- JavaScript类继承
和其他功能一样,ECMAScript 实现继承的方式不止一种.这是因为 JavaScript 中的继承机制并不是明确规定的,而是通过模仿实现的.这意味着所有的继承细节并非完全由解释程序处理.作为开发者 ...
- JavaScript中继承机制的模仿实现
首先,我们用一个经典例子来简单阐述一下ECMAScript中的继承机制. 在几何学上,实质上几何形状只有两种,即椭圆形(是圆形的)和多边形(具有一定数量的边).圆是椭圆的一种,它只有一个焦点.三角形. ...
- ECMAScript 继承机制实现
继承机制的实现 要用 ECMAScript 实现继承机制,您可以从要继承的基类入手.所有开发者定义的类都可作为基类.出于安全原因,本地类和宿主类不能作为基类,这样可以防止公用访问编译过的浏览器级的代码 ...
- 一文看懂JS继承
继承是OOP中大家最喜欢谈论的内容之一,一般来说,继承都两种方式:接口继承和实现继承而JavaScript中没有接口继承需要的方法,因此只能依靠实现继承.在讲继承的实现之前,我们首先来回顾一下什么是继 ...
- JS如何实现继承?
JS的继承是基于JS类的基础上的一种代码复用机制.换言之,有了代码,我们就不需要复制之前写好的方法,只要通过简捷的方式 复用之前自己写的或同事写的代码.比如一个弹出层,我们需要在上面做一些修改.同事写 ...
随机推荐
- 属性"XmlFileName"的代码生成失败
属性"XmlFileName"的代码生成失败.错误是:"未将对象引用设置到对象实例" 解决: 控件修改造成,关闭打开页面,重新生成
- 数据库导出到excel
项目结构同上一篇 泛型通用的写法 ExportExcel.java package excel; import java.io.OutputStream; import java.lang.refle ...
- GoWithTheFlow
GoWithTheFlow http://notes.jetienne.com/2011/07/17/gowiththeflow.js-async-flow-control-with-a-zen-to ...
- 浅谈RFID电子标签封装技术
1RFID技术概述 1.1RFID技术概念 RFID是RadioFrequencyIdentification的缩写,即射频识别技术,俗称电子标签.RFID射频识别是一种非接触式的自动识别技术,它通过 ...
- MSSQL SERVER 2008 R2 无法连接到数据库,用户sa登录失败,错误:18456
原因:勾选了强制实施密码策略,但是设置的密码很简单依然可以,比如:123456 这是为什么?原来,这个功能要用到NetValidatePasswordPolicy() API这个函数. (该功能只有在 ...
- Android清单文件具体解释(三)----应用程序的根节点<application>
<application>节点是AndroidManifest.xml文件里必须持有的一个节点,它包括在<manifest>节点下.通过<application>节 ...
- poj2125Destroying The Graph(最小割+输出方案)
题目请戳这里 题目大意:给一张有向图,现在要选择一些点,删掉图中的所有边.具体操作为:选择点i,可以选择删除从i出发的所有有向边或者进入i的所有有向边,分别有个代价ini和outi,求最小的代价删掉所 ...
- Objective-c 类实现 (@implementation)
在用@interface声明类之后,可以使用@implementation进行实类的实现.类的实现的具体语法如下: @implementation 类名 方法实现代码; @end; 实例: @impl ...
- js超简单日历
用原生js写了一个超级简单的日历.当做是练习js中的Date类型. 思路: 获取某个日期,根据年份计算出每个月的天数. 利用Date中的getDay()知道该月份的第一天为星期几. 循环创建表格,显示 ...
- asp.net连接ORACLE数据库
这段时间维护客户的一个系统,该系统使用的是ORACLE数据库,之前开发的时候用的都是MSSQL,并没有使用过ORACLE.这两种数据库虽然都是关系型数据库,但是具体的操作大有不同,这里作下记录. 连接 ...