14.C#编程指南-多态性
public class Shape { // A few example members public int X { get; private set; } public int Y { get; private set; } public int Height { get; set; } public int Width { get; set; } // Virtual method public virtual void Draw() { Console.WriteLine("Performing base class drawing tasks"); } } class Circle : Shape { public override void Draw() { // Code to draw a circle... Console.WriteLine("Drawing a circle"); base.Draw(); } } class Rectangle : Shape { public override void Draw() { // Code to draw a rectangle... Console.WriteLine("Drawing a rectangle"); base.Draw(); } } class Triangle : Shape { public override void Draw() { // Code to draw a triangle... Console.WriteLine("Drawing a triangle"); base.Draw(); } } class Program { static void Main(string[] args) { // Polymorphism at work #1: a Rectangle, Triangle and Circle // can all be used whereever a Shape is expected. No cast is // required because an implicit conversion exists from a derived // class to its base class. System.Collections.Generic.List<Shape> shapes = new System.Collections.Generic.List<Shape>(); shapes.Add(new Rectangle()); shapes.Add(new Triangle()); shapes.Add(new Circle()); // Polymorphism at work #2: the virtual method Draw is // invoked on each of the derived classes, not the base class. foreach (Shape s in shapes) { s.Draw(); } // Keep the console open in debug mode. Console.WriteLine("Press any key to exit."); Console.ReadKey(); } } /* Output: Drawing a rectangle Performing base class drawing tasks Drawing a triangle Performing base class drawing tasks Drawing a circle Performing base class drawing tasks */
多态性常视为自封装和继承之后,面向对象编程的第三个支柱。多态性具有两个截然不同的方面:
- 在运行时,在方法参数和集合或数组等位置,派生类的对象可以作为基类的对象处理。发生此情况时,该对象的声明类型不再与运行时类型相同。
- 基类可以定义并实现虚方法,派生类可以重写这些方法,即派生类提供自己的定义和实现。在运行时,客户端代码调用该方法,CLR查找对象的运行时类型,并调用虚方法的重写方法。因此,您可以在源代码中调用基类的方法,但执行该方法的派生类版本。
虚方法允许您以统一方式处理多组相关的对象。 例如,假定您有一个绘图应用程序,允许用户在绘图图面上创建各种形状。 您在编译时不知道用户将创建哪些特定类型的形状。 但应用程序必须跟踪创建的所有类型的形状,并且必须更新这些形状以响应用户鼠标操作。 您可以使用多态性通过两个基本步骤解决这一问题:
-
创建一个类层次结构,其中每个特定形状类均派生自一个公共基类。
-
使用虚方法通过对基类方法的单个调用来调用任何派生类上的相应方法。
首先,创建一个名为 Shape 的基类,并创建一些派生类,例如 Rectangle、Circle 和 Triangle。 为 Shape 类提供一个名为 Draw 的虚方法,并在每个派生类中重写该方法以绘制该类表示的特定形状。 创建一个 List<Shape> 对象,并向该对象添加 Circle、Triangle 和 Rectangle。 若要更新绘图图面,请使用 foreach 循环对该列表进行循环访问,并对其中的每个 Shape 对象调用 Draw 方法。 虽然列表中的每个对象都具有声明类型 Shape,但调用的将是运行时类型(该方法在每个派生类中的重写版本)。
public class Shape { // A few example members public int X { get; private set; } public int Y { get; private set; } public int Height { get; set; } public int Width { get; set; } // Virtual method public virtual void Draw() { Console.WriteLine("Performing base class drawing tasks"); } } class Circle : Shape { public override void Draw() { // Code to draw a circle... Console.WriteLine("Drawing a circle"); base.Draw(); } } class Rectangle : Shape { public override void Draw() { // Code to draw a rectangle... Console.WriteLine("Drawing a rectangle"); base.Draw(); } } class Triangle : Shape { public override void Draw() { // Code to draw a triangle... Console.WriteLine("Drawing a triangle"); base.Draw(); } } class Program { static void Main(string[] args) { // Polymorphism at work #1: a Rectangle, Triangle and Circle // can all be used whereever a Shape is expected. No cast is // required because an implicit conversion exists from a derived // class to its base class. System.Collections.Generic.List<Shape> shapes = new System.Collections.Generic.List<Shape>(); shapes.Add(new Rectangle()); shapes.Add(new Triangle()); shapes.Add(new Circle()); // Polymorphism at work #2: the virtual method Draw is // invoked on each of the derived classes, not the base class. foreach (Shape s in shapes) { s.Draw(); } // Keep the console open in debug mode. Console.WriteLine("Press any key to exit."); Console.ReadKey(); } } /* Output: Drawing a rectangle Performing base class drawing tasks Drawing a triangle Performing base class drawing tasks Drawing a circle Performing base class drawing tasks */
在C#中,每个类型都是多态的,因为包括用户定义类型在内的所有类型都继承自Object。
多态性概述
虚成员
当派生类从基类继承时,它会获得基类的所有方法、字段、属性和事件。派生类的设计器可以选择是否
- 重写基类中的虚拟成员。
- 继承最接近的基类方法而不重写它
- 定义隐藏基类实现的成员的新非虚实现
仅当基类成员声明为virtual或abstract时,派生类才能重写基类成员。派生成员必须使用override关键字显式指示该方法将参与虚调用。以下代码提供了一个示例:
public class BaseClass { public virtual void DoWork() { } public virtual int WorkProperty { get { return 0; } } } public class DerivedClass : BaseClass { public override void DoWork() { } public override int WorkProperty { get { return 0; } } }
字段不能是虚拟的,只有方法、属性、事件和索引器才可以是虚拟的。 当派生类重写某个虚拟成员时,即使该派生类的实例被当作基类的实例访问,也会调用该成员。 以下代码提供了一个示例:
DerivedClass B = new DerivedClass(); B.DoWork(); // Calls the new method. BaseClass A = (BaseClass)B; A.DoWork(); // Also calls the new method.
虚方法和属性允许派生类扩展基类,而无需使用方法的基类实现。 有关更多信息,请参见 使用 Override 和 New 关键字进行版本控制(C# 编程指南)。 接口提供另一种方式来定义将实现留给派生类的方法或方法集。 有关更多信息,请参见接口(C# 编程指南)和在类和接口之间选择。
使用新成员隐藏基类成员
如果希望派生成员具有与基类中的成员相同的名称,但又不希望派生成员参与虚调用,则可以使用new关键字。new关键字放置在要替换的类成员的返回类型之前。以下代码提供了一个示例:
public class BaseClass { public void DoWork() { WorkField++; } public int WorkField; public int WorkProperty { get { return 0; } } } public class DerivedClass : BaseClass { public new void DoWork() { WorkField++; } public new int WorkField; public new int WorkProperty { get { return 0; } } }
通过将派生类的实例强制转换为基类的实例,仍然可以从客户端代码访问隐藏的基类成员。例如:
DerivedClass B = new DerivedClass(); B.DoWork(); // Calls the new method. BaseClass A = (BaseClass)B; A.DoWork(); // Calls the old method.
阻止派生类重写虚拟成员
无论在虚拟成员和最初声明虚拟成员的类之间已声明了多少个类,虚拟成员永远都是虚拟的。如果类A声明了一个虚拟成员,类B从A派生,类C从类B派生,则类C继承该虚拟成员,并且可以选择重写它,而不管类B是否为该成员声明了重写。以下代码提供了示例:
public class A { public virtual void DoWork() { } } public class B : A { //public override void DoWork() { } } public class C:B { public override void DoWork() { } }
派生类可以通过将重写声明为sealed来停止虚拟继承。这需要在类成员声明中的override关键字前面放置sealed关键字。以下代码提供了一个示例:
public class C : B { public sealed override void DoWork() { } }
在上一个示例中,方法 DoWork 对从 C 派生的任何类都不再是虚拟方法。 即使它们转换为类型 B 或类型 A,它对于 C 的实例仍然是虚拟的。 通过使用 new 关键字,密封的方法可以由派生类替换,如下面的示例所示:
public class D : C { public new void DoWork() { } }
在此情况下,如果在 D 中使用类型为 D 的变量调用 DoWork,被调用的将是新的 DoWork。 如果使用类型为 C、B 或 A 的变量访问 D 的实例,对 DoWork 的调用将遵循虚拟继承的规则,即把这些调用传送到类 C 的 DoWork 实现。
从派生类访问基类虚拟成员
已替换或重写某个方法或属性的派生类仍然可以使用base关键字访问基类的访问方法或属性。以下代码提供了一个示例:
public class Base { public virtual void DoWork() {/*...*/ } } public class Derived : Base { public override void DoWork() { //Perform Derived's work here //... // Call DoWork on base class base.DoWork(); } }
使用Override和New关键字进行版本控制
C#语言经过专门设计,以便不同库中的基类与派生类之间的版本控制可以不断向前发展,同时保持向后兼容。这具有多方面的意义。例如,这意味着在基类中引入与派生类中的某个成员具有相同名称的新成员在C#中是完全支持的,不会导致意外行为。它还意味着类必须显式声明某方法是要重写一个继承主法,还是一个隐藏具有类似名称的继承方法的新方法。
在C#中,派生类可以包含与基类方法同名的方法。
- 基类方法必须定义为virtual。
- 如果派生类中的方法前面没有new或override关键字,则编译器将发出警告,该方法将有如存在new关键字一样执行操作。
- 如果派生类中的方法前面带有new关键字,则该方法被定义为独立于基类中的方法。
- 如果派生类中的方法前面带有override关键字,则派生类的对象将调用该方法,而不是调用基类方法。
- 可以从派生类中使用base关键字调用基类方法。
- override、virtual和new关键字还可以用于属性、索引器和事件中。
默认情况下,C#方法为非虚方法。如果某个方法被声明为虚方法,则继承该方法的任何类都可以实现它自己的版本。若要使方法成为虚方法,必须在基类的方法声明中使用virtual修饰符。然后,派生类可以使用override关键字重写基虚方法,或使用new关键字隐藏基类中的虚方法。如果override关键字和new关键字均未指定,编译器将发出警告,并且派生类中的方法将隐藏基类中的方法。
重写与方法选择
当在类中指定方法时,如果有多个方法与调用兼容(例如,存在两种同名的方法,并且其参数与传递的参数兼容),则 C# 编译器将选择最佳方法进行调用。 下面的方法将是兼容的:
public class Derived : Base { public override void DoWork(int param) { } public void DoWork(double param) { } }
在 Derived 的一个实例中调用 DoWork 时,C# 编译器将首先尝试使该调用与最初在 Derived 上声明的 DoWork 版本兼容。 重写方法不被视为是在类上进行声明的,而是在基类上声明的方法的新实现。 仅当 C# 编译器无法将方法调用与 Derived 上的原始方法匹配时,它才尝试将该调用与具有相同名称和兼容参数的重写方法匹配。 例如:
int val = 5; Derived d = new Derived(); d.DoWork(val); // Calls DoWork(double).
由于变量 val 可以隐式转换为 double 类型,因此 C# 编译器将调用 DoWork(double),而不是 DoWork(int)。 有两种方法可以避免此情况。 首先,避免将新方法声明为与虚方法同名。 其次,可以通过将 Derived 的实例强制转换为 Base 来使 C# 编译器搜索基类方法列表,从而使其调用虚方法。 由于是虚方法,因此将调用 Derived 上的 DoWork(int) 的实现。 例如:
((Base)d).DoWork(val); // Calls DoWork(int) on Derived.
了解何时使用Override和New关键字
在C#中,派生类中方法的名称可与基类中方法的名称相同。可通过使用new和override关键字指定方法互动的方式。override修饰符extends基类方法,且new修饰符将其“隐藏”起来。
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace OverrideAndNew2 { class Program { static void Main(string[] args) { // Declare objects of the derived classes and test which version // of ShowDetails is run, base or derived. TestCars1(); // Declare objects of the base class, instantiated with the // derived classes, and repeat the tests. TestCars2(); // Declare objects of the derived classes and call ShowDetails // directly. TestCars3(); // Declare objects of the base class, instantiated with the // derived classes, and repeat the tests. TestCars4(); } public static void TestCars1() { System.Console.WriteLine("\nTestCars1"); System.Console.WriteLine("----------"); Car car1 = new Car(); car1.DescribeCar(); System.Console.WriteLine("----------"); // Notice the output from this test case. The new modifier is // used in the definition of ShowDetails in the ConvertibleCar // class. ConvertibleCar car2 = new ConvertibleCar(); car2.DescribeCar(); System.Console.WriteLine("----------"); Minivan car3 = new Minivan(); car3.DescribeCar(); System.Console.WriteLine("----------"); } // Output: // TestCars1 // ---------- // Four wheels and an engine. // Standard transportation. // ---------- // Four wheels and an engine. // Standard transportation. // ---------- // Four wheels and an engine. // Carries seven people. // ---------- public static void TestCars2() { System.Console.WriteLine("\nTestCars2"); System.Console.WriteLine("----------"); var cars = new List<Car> { new Car(), new ConvertibleCar(), new Minivan() }; foreach (var car in cars) { car.DescribeCar(); System.Console.WriteLine("----------"); } } // Output: // TestCars2 // ---------- // Four wheels and an engine. // Standard transportation. // ---------- // Four wheels and an engine. // Standard transportation. // ---------- // Four wheels and an engine. // Carries seven people. // ---------- public static void TestCars3() { System.Console.WriteLine("\nTestCars3"); System.Console.WriteLine("----------"); ConvertibleCar car2 = new ConvertibleCar(); Minivan car3 = new Minivan(); car2.ShowDetails(); car3.ShowDetails(); } // Output: // TestCars3 // ---------- // A roof that opens up. // Carries seven people. public static void TestCars4() { System.Console.WriteLine("\nTestCars4"); System.Console.WriteLine("----------"); Car car2 = new ConvertibleCar(); Car car3 = new Minivan(); car2.ShowDetails(); car3.ShowDetails(); } // Output: // TestCars4 // ---------- // Standard transportation. // Carries seven people. } // Define the base class, Car. The class defines two virtual methods, // DescribeCar and ShowDetails. DescribeCar calls ShowDetails, and each derived // class also defines a ShowDetails method. The example tests which version of // ShowDetails is used, the base class method or the derived class method. class Car { public virtual void DescribeCar() { System.Console.WriteLine("Four wheels and an engine."); ShowDetails(); } public virtual void ShowDetails() { System.Console.WriteLine("Standard transportation."); } } // Define the derived classes. // Class ConvertibleCar uses the new modifier to acknowledge that ShowDetails // hides the base class method. class ConvertibleCar : Car { public new void ShowDetails() { System.Console.WriteLine("A roof that opens up."); } } // Class Minivan uses the override modifier to specify that ShowDetails // extends the base class method. class Minivan : Car { public override void ShowDetails() { System.Console.WriteLine("Carries seven people."); } } }