多形性

多态性常被视为自封装和继承之后,面向对象的编程的第三个支柱。 Polymorphism(多态性)是一个希腊词,指“多种形态”,多态性具有两个截然不同的方面:

  • 在运行时,派生类的对象可以在方法参数、集合或数组等位置被视为基类的对象。 在出现此多形性时,该对象的声明类型不再与运行时类型相同。
  • 基类可以定义和实现 虚拟方法,派生类可以 重写 它们,这意味着它们提供自己的定义和实现。 在运行时,客户端代码调用该方法,CLR 查找对象的运行时类型,并调用虚方法的重写方法。 你可以在源代码中调用基类的方法,执行该方法的派生类版本。

虚方法允许你以统一方式处理多组相关的对象。 例如,假定你有一个绘图应用程序,允许用户在绘图图面上创建各种形状。 你在编译时不知道用户将创建哪些特定类型的形状。 但应用程序必须跟踪创建的所有类型的形状,并且必须更新这些形状以响应用户鼠标操作。 你可以使用多态性通过两个基本步骤解决这一问题:

  1. 创建一个类层次结构,其中每个特定形状类均派生自一个公共基类。
  2. 使用虚方法通过对基类方法的单个调用来调用任何派生类上的相应方法。

首先,创建一个名为 ShapeRectangle 的基类,并创建一些派生类,例如 CircleTriangle、 和 。 为 Shape 类提供一个名为 Draw 的虚拟方法,并在每个派生类中重写该方法以绘制该类表示的特定形状。 创建 List<Shape> 对象,并向其添加 CircleTriangleRectangle

public class Shape
{
    // A few example members
    public int X { get; init; }
    public int Y { get; init; }
    public int Height { get; init; }
    public int Width { get; init; }

    // Virtual method
    public virtual void Draw()
    {
        Console.WriteLine("Performing base class drawing tasks");
    }
}

public class Circle : Shape
{
    public override void Draw()
    {
        // Code to draw a circle...
        Console.WriteLine("Drawing a circle");
        base.Draw();
    }
}
public class Rectangle : Shape
{
    public override void Draw()
    {
        // Code to draw a rectangle...
        Console.WriteLine("Drawing a rectangle");
        base.Draw();
    }
}
public class Triangle : Shape
{
    public override void Draw()
    {
        // Code to draw a triangle...
        Console.WriteLine("Drawing a triangle");
        base.Draw();
    }
}

若要更新绘图图面,请使用 foreach 循环对该列表进行循环访问,并对其中的每个 Draw 对象调用 Shape 方法。 虽然列表中的每个对象都具有声明类型 Shape,但调用的将是运行时类型(该方法在每个派生类中的重写版本)。

// Polymorphism at work #1: a Rectangle, Triangle and Circle
// can all be used wherever a Shape is expected. No cast is
// required because an implicit conversion exists from a derived
// class to its base class.
List<Shape> shapes =
[
    new Rectangle(),
    new Triangle(),
    new Circle()
];

// Polymorphism at work #2: the virtual method Draw is
// invoked on each of the derived classes, not the base class.
foreach (var shape in shapes)
{
    shape.Draw();
}
/* 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

多形性概述

虚拟成员

当派生类从基类继承时,它包括基类的所有成员。 基类中声明的所有行为都是派生类的一部分。 这使派生类的对象能够被视为基类的对象。 访问修饰符(publicprotectedprivate等)确定是否可从派生类实现访问这些成员。 虚拟方法为设计器提供了派生类行为的不同选择:

  • 派生类可以重写基类中的虚拟成员,从而定义新行为。
  • 派生类可以继承最近的基类方法,而无需重写它,保留现有行为,但允许进一步派生类重写该方法。
  • 派生类可以定义隐藏基类实现的成员的新非虚拟实现。

仅当基类成员声明为 virtualabstract 时,派生类才能重写基类成员。 派生成员必须使用 override 关键字显式指示该方法将参与虚调用。 以下代码提供了一个示例:

public class BaseClass
{
    public virtual void DoWork() { }
    public virtual int WorkProperty => 0;
}
public class DerivedClass : BaseClass
{
    public override void DoWork() { }
    public override int WorkProperty
    {
        get { return 0; }
    }
}

字段不能是虚拟的,只有方法、属性、事件和索引器才可以是虚拟的。 当派生类重写某个虚拟成员时,即使该派生类的实例被当作基类的实例访问,也会调用该成员。 以下代码提供了一个示例:

DerivedClass B = new();
B.DoWork();  // Calls the new method.

BaseClass A = B;
A.DoWork();  // Also calls the new method.

虚方法和属性允许派生类扩展基类,而无需使用方法的基类实现。 有关详细信息,请参阅使用 Override 和 New 关键字进行版本控制。 接口提供另一种方式来定义将实现留给派生类的方法或方法集。

使用新成员隐藏基类成员

如果希望派生类具有与基类中的成员同名的成员,则可以使用 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; }
    }
}

使用new关键字时,将创建一个隐藏基类方法的方法,而不是重写它。 这不同于虚拟方法。 使用方法隐藏时,调用的方法取决于变量的编译时类型,而不是对象的运行时类型。

若要从客户端代码访问隐藏的基类成员,可以将派生类的实例转换为基类的实例。 例如:

DerivedClass B = new();
B.DoWork();  // Calls the new method.

BaseClass A = (BaseClass)B;
A.DoWork();  // Calls the old method.

在此示例中,这两个变量都引用相同的对象实例,但调用的方法取决于变量的声明类型: DerivedClass.DoWork() 通过 DerivedClass 变量访问时,以及 BaseClass.DoWork() 通过 BaseClass 变量访问时。

阻止派生类重写虚拟成员

虚拟成员始终保持虚拟状态,无论在虚拟成员与最初声明它的类之间声明了多少个类。 如果类A声明虚拟成员,并且类B派生自AC类,则类BC继承虚拟成员,并可能重写它,而不管类B是否为该成员声明了重写。 以下代码提供了一个示例:

public class A
{
    public virtual void DoWork() { }
}
public class B : A
{
    public override void DoWork() { }
}

派生类可以通过将重写声明为 sealed 来停止虚拟继承。 停止继承需要在类成员声明中的 sealed 关键字前面放置 override 关键字。 以下代码提供了一个示例:

public class C : B
{
    public sealed override void DoWork() { }
}

在上一个示例中,方法 DoWork 对从 C 派生的任何类都不再是虚拟方法。 即使它们转换为类型 C 或类型 B,它对于 A 的实例仍然是虚拟的。 通过使用 new 关键字,密封的方法可以由派生类替换,如下面的示例所示:

public class D : C
{
    public new void DoWork() { }
}

在此情况下,如果在 DoWork 中使用类型为 D 的变量调用 D,被调用的将是新的 DoWork。 如果类型为CBA的变量用于访问D实例,那么对DoWork的调用遵循虚拟继承的规则,将这些调用路由到类CDoWork的实现。

从派生类访问基类虚拟成员

替换或替代方法或属性的派生类仍然可以使用 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();
    }
}

有关详细信息,请参阅 base

注意

建议虚拟成员在 base 自己的实现中调用该成员的基类实现。 允许基类行为发生使得派生类能够集中精力实现特定于派生类的行为。 如果未调用基类实现,则派生类可以使其行为与基类的行为兼容。