第 37 讲:面向对象编程(九):成员继承控制
前面我们讲了继承控制的基本使用方式和手段,今天我们来说一下,跟成员有关系的继承控制方式,以及对应的关键字。
Part 1 abstract 和 override 成员修饰符
前面我们说到 abstract 类修饰符,表示类不再允许实例化,使用 new 语句产生这个类型的对象。可问题在于,以类为单位控制的继承实在是没有多大的用途。下面我们来介绍 abstract 修饰符修饰的成员。
在抽象类(abstract class 组合)里,有一种特殊的成员类别,叫抽象成员。这是别的类型不具备的修饰方式。我们可直接在抽象类里,对某个或某些成员追加 abstract 修饰符,以表达这个成员“我这个类不给实现逻辑”;而具体的实现逻辑丢给派生类。
这个思维是我们第一次听说,因此我们来举例说明。假设我有一个 Shape 的抽象类,表示一个形状。显然,形状肯定会有一些相关的属性信息,比如说形状本身的面积。假设我们定为 Area。那么我们创建一些子类型,从 Shape 类型派生。
我们创建了两个子类型 Square 和 Circle 类型,从 Shape 派生。在 Shape 里有一个 Area 属性。可以注意语法不是很好理解的地方在于这个 get 后直接跟了一个分号。这是正确的写法,是因为这个 Area 的 get 方法(取值器)目前是无法写出来的,因为我都不知道这个 Shape 到底需要什么信息才可计算面积。比如正方形需要一个参数(边长)就可以计算面积;但长方形需要两个参数(长和宽)才可以计算;梯形则需要三个参数(上底、下底和高)。
因为参数不定,所以我们无法从基类型 Shape 上给出代码实现。于是,我们定成抽象属性,以表达这个属性只能且必须在子类型是实现。
在子类型 和 Circle 里,我们不得不对 Area 给出实现的代码。另外,我们要对上面的抽象类型的 Area 属性实现执行代码,因此我们需要在这个属性前面追加 override 关键字。override 专门用来表达“我是把基类型的同名成员拿来使用和修改的”,在一般情况上是不使用这个关键字的。
const修饰符可修饰一个系统自带的那些数据类型的字面量。它和readonly目前的唯一区别是,const只能修饰一个字面量赋值过来的字段,且不能修饰static修饰符;而readonly可以修饰任何数据类型赋值过来的字段,且可以有static也可以没有static。
Part 2 sealed 和 override 成员修饰符
正相反的是,如果类型给出了实现,那么就需要用 override 修饰符对成员进行修饰。但是,override 修饰后的成员还可继续在往下继承的类型里被重写和修改逻辑。比如说 A 类型有个 Prop 属性,是 abstract 修饰的;接着 B 从 A 类型派生,那么 Prop 属性在 B 类型里就必须重写和修改逻辑,因此要标记 override 修饰符。可继承是可以不限制的,所以如果还有个 C 类型走 B 类型派生的话,那么 C 类型里也可重写 B 类型里的 Prop 的实现逻辑。
假设 B 类型给出的实现不可变动,不让后续继承的时候修改的话,我们可使用 sealed 关键字标记这个成员(构成 sealed override 的关键字组合),来表达这个成员是重写的,但在这里重写后,后续只能使用这个成员,而不可修改执行逻辑。
比如这样写代码的话,C 类型里给出 Prop 的实现,因为 C 从 B 类型派生,但 B 类型里 Prop 属性就标记了 sealed 关键字了,因此这个成员不再允许后续继承的时候再次重写了,所以 C 类型里 Prop 的属性就会产生编译器错误,编译器会提示你“不能这么用”。
Part 3 virtual 和 override 成员修饰符
abstract 的好处在于,可以把成员作为单位提供抽象的书写模式,编译器就会自动提示我们让我们在子类型里实现它们,不论对于代码实现来说,还是代码书写来说,都非常方便。但是,abstract 有一个问题是,如果我基类型想要给出默认的实现代码的话(而不是一定非要丢给派生类来实现,我们可先给出一个默认实现,然后派生类按自己是否需要修改来决定到底需不需要 override 重写和修改掉原始的逻辑)。
比如说前面举的例子 Car(汽车)类型和 ConvertibleCar(敞篷跑车)类型。我们显然可以给 Car 提供一个默认的实现代码来输出汽车的具体描述。
这样写肯定是没错的,但问题就在于这里的 new 关键字改变了继承的关系。new 关键字是表示“我子类型里提供一个同名的方法,而这个方法和基类型的那个方法没有任何关系”。这怎么可能?基类型的 GetDescription 显然是照搬下来并重新修改逻辑才对。因此,我们这里必须改用 override 修饰符。
问题来了。基类型的 GetDescription 成员并没有修饰 abstract,因为它本身就是自带了执行的逻辑的,如果我们给子类型的 GetDescription 方法标记 override 这不就是错了么。
那么,怎么解决两边都没办法解决的问题呢?我们只需要在基类型 Car 里的 GetDescription 方法上标记 virtual,以表达“这个方法可提供给派生类型重写和修改逻辑”。
这样的话,基类型 Car 里的 GetDescription 方法就表示“我有自己的实现代码,你可以用也可以修改逻辑”。如果你需要修改执行代码的话,这里你需要为子类型重写的这个同名方法 GetDescription 上追加 override 修饰符,来表示“我是确确实实是从基类型里拿下来的成员,且要修改执行逻辑的”。
Part 4 使用 base 对象调用基类型成员的执行逻辑
假设一种情况。我这里有两个类,一个叫做 Person,表示一个人,而另外有一个类叫 Employee 类,表示雇员(社会人)。
那么,请注意最下面的 Main 方法,执行了 e 实例(Employee 类型的实例)的 GetInfo 方法,会输出什么呢?
我们定位到 Employee 里面的 GetInfo 方法上去。里面写了一句 base.GetInfo(),还记得之前说过的 base 关键字吗?是的,base 关键字用来表示基类型的成员的使用。但是因为前面没有提到继承的控制,因此 base 和 this 的效果是一样的。不过这里不一样的地方是,因为 GetInfo 是被重写过的,因此写的 base.GetInfo() 一定指的是基类型的那个 GetInfo。因此,这句话等效于把基类型的 GetInfo 的执行代码直接给抄下来。
因此,整体输出三句话:第一个是输出 name,第二个是输出 ssn,最后是重写的 GetInfo 方法里输出的 id。
Part 5 一些问题
下面针对于前文介绍和的内容,给出一些可能你有疑惑的问题。
5-1 abstract、sealed 和 virtual 修饰符的适用范围
显然,并非所有成员都可以修饰这些东西。比如说字段。字段并不是一种执行逻辑,而是一个数值。而 static 修饰过的成员也都不能使用这些关键字。因为继承机制显然是跟对象绑定起来才有意义。比如,“工人”和“老师”可能不是同一个类型的实例,比如有可能“工人”是 Worker 类型的,而“老师”则可能是 Teacher 类型的。但是,它们俩都从 Person 类型派生下来,这个 Person 类型可能包含了一些可以使用的成员,比如 Eat 方法表示吃饭过程;Sleep 方法表示睡觉过程;GoShopping 表示购物血拼的过程。但是,如果是静态的成员的话,这一切就显得没有意义。因为静态的话,并不会在逻辑上跟一个单独的对象绑定起来,此时说什么吃饭购物都显得没有了意义,那你还抽象、重写它们干什么呢?
因此,这三个修饰符的适用范围显然是:
不能是字段;
不能被
static修饰符过的成员。
5-2 override 和 new 的区别
可以看到,前面三个小节分别说的是 abstract、sealed 和 virtual 关键字,但因为它们都可搭配 override 关键字一并使用,所以标题里全都带有 override 修饰符这个东西。而前面我们也说了 new 修饰符。由于它们俩的功能看起来差不太多,所以下面我们来说一下这两个关键字的区别。
override修饰符:这个成员是我从基类型拿下来的同名成员,因此我这里是用来修改原始类型成员的一种机制;new修饰符:这个成员和基类型成员的实现没有半点关系。我这里给出的实现会让基类型这个成员完全被覆盖掉,阻断了这个成员在整个继承链上的继承情况。
要知道,继承关系是可无限往下的,A 可以派生出 B,然后 B 还能派生出 C,甚至 C 还能派生出 D 类型。如果在 B 类型里用到 override 关键字修饰了某个成员,就表达 B 是重写了 A 的这个成员,修改掉了里面的逻辑;但是如果 B 类型某个成员用的是 new 关键字,那么 A 类型的这个同名成员和 B 这个成员一点关系都没有了。以后,C 和 D 类型里的这个成员都以 B 这个成员为准,而 A 里的同名成员被 B 类型用 new 修饰符覆盖掉了。

