c#高级编程 继承 (三)
面向对象的三个最重要的概念是继承、封装和多态性。本问的重点是继承和多态性。当某类型的成员声明为private时,不能从外部访问,它们封装在类型中。
1、继承的类型
首先介绍一些面向对象(Object-Oriented, OO)术语,看看C#在继承方面支持和不支持的功能。
● 单重继承: 表示一个类可以派生自一个基类。C#就采用这种继承。
● 多重继承: 多重继承允许一个类派生自多个类。C#不支持类的多重继承,但允许接口的多重继承。
● 多层继承: 多层继承允许继承有更大的层次结构。类B派生自类A,类C又派生自类B。其中,类B也称为中间基类,C#支持它,也很常用。
● 接口继承: 定义了接口的继承。这里允许多重继承。
1.1 多重继承
一些语言(如C++)支持所谓的“多重继承”,即一个类派生自多个类。对于实现继承,多重继承会给生成的代码增加复杂性,还会带来一些开销。因此,C#的设计人员决定不支持类的多重继承,因为支持多重继承会增加复杂性,还会带来一些开销。
而C#又允许类型派生自多个接口。一个类型可以实现多个接口。这说明,C#类可以派生自另一个类和任意多个接口。更准确地说,因为System.Object是一个公共的基类,所以每个C#类(除了Object类之外)都有一个基类,还可以有任意多个基接口。
1.2 结构和类
使用结构的一个限制是结构不支持继承,但每个结构都自动派生自System.ValueType。不能编码实现结构的类型层次,但结构可以实现接口。换言之,结构并不支持实现继承,但支持接口继承。定义的结构和类可以总结为:
● 结构总是派生自System.ValueType,它们还可以派生自任意多个接口。
● 类总是派生自System.Object或用户选择的另一个类,它们还可以派生自任意多个接口。
2、实现继承
如果要声明派生自另一个类的一个类,就可以使用下面的语法:
class MyDerivedClass: MyBaseClass { // members }
如果类(或结构)也派生自接口,则用逗号分隔列表中的基类和接口:
public class MyDerivedClass: MyBaseClass, IInterface1, IInterface2 { // members }
如果类和接口都用于派生,则类总是必须放在接口的前面。
对于结构,语法如下(只能用于接口继承):
public struct MyDerivedStruct: IInterface1, IInterface2 { // members }
如果在类定义中没有指定基类,C#编译器就假定System.Object是基类。因此,派生自Object类(或使用object关键字),与不定义基类的效果是相同的。
下面的例子定义了基类Shape。无论是矩形还是椭圆,形状都有一些共同点:形状都有位置和大小。定义相应的类时,位置和大小应包含在Shape类中。Shape类定义了只读属性Position和Shape,它们使用自动属性初始化器来初始化:
public class Position { public int X { get; set; } public int Y { get; set; } } public class Size { public int Width { get; set; } public int Height { get; set; } } public class Shape { public Position Position { get; } = new Position(); public Size Size { get; } = new Size(); }
3、虚方法
把一个基类方法声明为virtual,就可以在任何派生类中重写该方法:
public class Shape { public virtual void Draw() { WriteLine($"Shape with {Position} and {Size}"); } }
如果实现代码只有一行,在C# 6中,也可以把virtual关键字和表达式体的方法(使用lambda运算符)一起使用。这个语法可以独立于修饰符,单独使用:
public class Shape { public virtual void Draw() => WriteLine($"Shape with {Position} and {Size}"); }
也可以把属性声明为virtual。对于虚属性或重写属性,语法与非虚属性相同,但要在定义中添加关键字virtual,其语法如下所示:
public virtual Size Size { get; set; }
当然,也可以给虚属性使用完整的属性语法:
private Size _size; public virtual Size Size { get { return _size; }set { size = value; } }
为了简单起见,下面的讨论将主要集中于方法,但其规则也适用于属性。
C#中虚函数的概念与标准OOP的概念相同:可以在派生类中重写虚函数。在调用方法时,会调用该类对象的合适方法。在C#中,函数在默认情况下不是虚拟的,但(除了构造函数以外)可以显式地声明为virtual。
C#要求在派生类的函数重写另一个函数时,要使用override关键字显式声明:
public class Rectangle : Shape { public override void Draw() => WriteLine($"Rectangle with {Position} and {Size}"); }
注意: 重写基类的方法时,签名(所有参数类型和方法名)和返回类型必须完全匹配。否则,以后创建的新成员就不覆盖基类成员。
成员字段和静态函数都不能声明为virtual,因为这个概念只对类中的实例函数成员有意义。
4、多态性
使用多态性,可以动态地定义调用的方法,而不是在编译期间定义。编译器创建一个虚拟方法表(vtable),其中列出了可以在运行期间调用的方法,它根据运行期间的类型调用方法。
在下面的例子中,DrawShape()方法接收一个Shape参数,并调用Shape类的Draw()方法:
public static void DrawShape(Shape shape) { shape.Draw(); }
使用之前创建的矩形调用方法。尽管方法声明为接收一个Shape对象,但任何派生Shape的类型(包括Rectangle)都可以传递给这个方法:
var r = new Rectangle(); r.Position.X = 33; r.Position.Y = 22; r.Size.Width = 200; r.Size.Height = 100; DrawShape(r);
运行这个程序,查看Rectangle.Draw方法()而不是Shape.Draw()方法的输出。输出行从Rectangle开始。如果基类的方法不是虚拟方法或没有重写派生类的方法,就使用所声明对象(Shape)的类型的Draw()方法,因此输出从Shape开始:
Rectangle with X: 33, y: 22 and Width: 200, Height: 100
5、隐藏方法
如果签名相同的方法在基类和派生类中都进行了声明,但该方法没有分别声明为virtual和override,派生类方法就会隐藏基类方法。
在大多数情况下,是要重写方法,而不是隐藏方法,因为隐藏方法会造成对于给定类的实例调用错误方法的危险。但是,如下面的例子所示,C#语法可以确保开发人员在编译时收到这个潜在错误的警告,从而使隐藏方法(如果这确实是用户的本意)更加安全。
这也是类库开发人员得到的版本方面的好处。
假定有一个类Shape:
public class Shape { // various members }
在将来的某一刻,要编写一个派生类Ellipse,用它给Shape基类添加某个功能,特别
是要添加该基类中目前没有的方法——MoveBy():
public class Ellipse: Shape { public void MoveBy(int x, int y) { Position.X += x; Position.Y += y; } }
过了一段时间,基类的编写者决定扩展基类的功能。为了保持一致,他也添加了一个名为MoveBy()的方法,该方法的名称和签名与前面添加的方法相同,但并不完成相同的工作。这个新方法可能声明为virtual,也可能不是。
如果重新编译派生的类,会得到一个编译器警告,因为出现了一个潜在的方法冲突。
然而,也可能使用了新的基类,但没有编译派生类;只是替换了基类程序集。基类程序集可以安装在全局程序集缓存中(许多Framework程序集都安装在此)。
现在假设基类的MoveBy()方法声明为虚方法,基类本身调用MoveBy()方法。会调用哪个方法?基类的方法还是前面定义的派生类的MoveBy()方法?因为派生类的MoveBy()方法没有用override关键字定义(这是不可能的,因为基类MoveBy()方法以前不存在),编译器假定派生类的MoveBy()方法是一个完全不同的方法,与基类的方法没有任何关系,只是名字相同。这种方法的处理方式就好像它有另一个名称一样。
编译Ellipse类会生成一个编译警告,提醒使用new关键词隐藏方法。在实践中,不使用new关键字会得到相同的编译结果,但避免出现编译器警告:
public class Ellipse: Shape { new public void Move(Position newPosition) { Position.X = newPosition.X; Position.Y = newPosition.Y; } //. . . other members }
不使用new关键字,也可以重命名方法,或者,如果基类的方法声明为virtual,且用作相同的目的,就重写它。然而,如果其他方法已经调用了此方法,简单的重命名会破坏其他代码。
6、调用方法的基类版本
C#有一种特殊的语法用于从派生类中调用方法的基类版本:base.<MethodName>()。例如,派生类Shape声明了Move()方法,想要在派生类Rectangle中调用它,以使用基类的实现代码。为了添加派生类中的功能,可以使用base调用它:
public class Shape { public virtual void Move(Position newPosition) { Position.X = newPosition.X; Position.Y = newPosition.Y; WriteLine($"moves to {Position}"); } //. . . other members }
Move()方法在Rectangle类中重写,把Rectangle一词添加到控制台。写出文本之后,使用base关键字调用基类的方法:
public class Rectangle: Shape { public override void Move(Position newPosition) { Write("Rectangle "); base.Move(newPosition); } //. . . other members }
现在,矩形移动到一个新位置:
r.Move(new Position { X = 120, Y = 40 });
运行应用程序,输出是Rectangle和Shape类中Move()方法的结果:
Rectangle moves to X: 120, Y: 40
注意: 使用base关键字,可以调用基类的任何方法——而不仅仅是已重写的方法。
7、抽象类和抽象方法
C#允许把类和方法声明为abstract。抽象类不能实例化,而抽象方法不能直接实现,必须在非抽象的派生类中重写。显然,抽象方法本身也是虚拟的。如果类包含抽象方法,则该类也是抽象的,也必须声明为抽象的。
下面把Shape类改为抽象类。因为其他类需要派生自这个类。新方法Resize声明为抽象,因此它不能有在Shape类中的任何实现代码:
public abstract class Shape { public abstract void Resize(int width, int height); // abstract method }
从抽象基类中派生类型时,需要实现所有抽象成员。否则,编译器会报错:
public class Ellipse : Shape{ public override void Resize(int width, int height) { Size.Width = width; Size.Height = height; } }
当然,实现代码也可以如下面的例子所示。抛出类型NotImplementationException的异常也是一种实现方式,在开发过程中,它通常只是一个临时的实现:
public override void Resize(int width, int height) { throw new NotImplementedException(); }
使用抽象的Shape类和派生的Ellipse类,可以声明Shape的一个变量。不能实例化它,但是可以实例化Ellipse,并将其分配给Shape变量:
Shape s1 = new Ellipse(); DrawShape(s1);
8、密封类和密封方法
如果不应创建派生自某个自定义类的类,该自定义类就应密封。给类添加sealed修饰符,就不允许创建该类的子类。密封一个方法,表示不能重写该方法。
sealedclass FinalClass { // etc } class DerivedClass: FinalClass // wrong. Cannot derive from sealed class . { // etc }
在把类或方法标记为sealed时,最可能的情形是:如果在库、类或自己编写的其他类的操作中,类或方法是内部的,则任何尝试重写它的一些功能,都可能导致代码的不稳定。
密封类有另一个原因。对于密封类,编译器知道不能派生类,因此用于虚拟方法的虚拟表可以缩短或消除,以提高性能。string类是密封的。
将一个方法声明为sealed的目的类似于一个类。方法可以是基类的重写方法,但是在接下来的例子中,编译器知道,另一个类不能扩展这个方法的虚拟表;它在这里终止继承。
class MyClass: MyBaseClass { public sealed override void FinalMethod() { // implementation} } class DerivedClass: MyClass { public override void FinalMethod() // wrong. Will give compilation error { } }
要在方法或属性上使用sealed关键字,必须先从基类上把它声明为要重写的方法或属性。如果基类上不希望有重写的方法或属性,就不要把它声明为virtual。
9、派生类的构造函数
这个没搞懂,后续完善
拓展:c#修饰符