Polymorphism

多型通常是指物件導向程式設計的第三個重要部分,其重要性僅次於封裝和繼承。 多型在希臘文中表示「多種形狀」,可分成下列兩方面:

  • 在執行階段,衍生類別物件可視為方法參數和集合或陣列等位置中的基底類別物件。 發生此多型時,物件的宣告型別與其執行階段型別將不再相同。
  • 基底類別可以定義和實作 virtual「方法」,而衍生類別可以覆寫這些方法,換句話說,衍生類別會提供自己的定義和實作。 在執行階段,當用戶端程式碼呼叫方法時,CLR 會查詢物件的執行階段類型,然後叫用虛擬方法的覆寫。 在原始程式碼中,您可以在基底類別上呼叫方法,然後執行衍生類別版本的方法。

虛擬方法可讓您以一致的方式來使用相關物件群組。 例如,假設您有一個繪圖應用程式,可讓使用者在繪圖介面上建立各種圖形。 您不知道使用者將在編譯時期建立哪一種圖形。 但是,應用程式必須追蹤所建立的所有不同圖形類型,並且必須根據使用者滑鼠動作來更新圖形。 您可以使用多型,分兩個基本步驟來解決這個問題:

  1. 建立類別階層架構,其中每個特定圖形類別都是衍生自一個通用基底類別。
  2. 使用虛擬方法,透過對基底類別方法發出單一呼叫,在任何衍生類別上叫用適當的方法。

首先,建立稱為 Shape 的基底類別,以及 RectangleCircleTriangle 等衍生類別。 將稱為 Shape 的虛擬方法提供給 Draw 類別,然後在每個衍生類別中覆寫此方法,以繪製該類別代表的特定圖形。 建立 List<Shape> 物件,並將 CircleTriangleRectangle 新增至其中。

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");
    }
}

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 迴圈逐一查看清單,並在清單中的每個 Shape 物件上呼叫 Draw 方法。 即使清單中的每個物件都有 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.
var shapes = new List<Shape>
{
    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
    {
        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 = 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; }
    }
}

您仍然可以透過將衍生類別執行個體轉換成基底類別執行個體,從用戶端程式碼存取隱藏的基底類別成員。 例如:

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 是否宣告該成員的覆寫,類別 C 都可以選擇覆寫該成員。 下列程式碼提供一個範例:

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() { }
}

在這個案例中,如果使用型別 D 的變數在 D 上呼叫 DoWork,則會呼叫新的 DoWork。 如果使用型別 CBA 的變數來存取 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();
    }
}

如需詳細資訊,請參閱 base

注意

建議虛擬成員使用 base,在其所擁有的實作中呼叫其基底類別實作。 允許發生基底類別行為,可讓衍生類別集中實作衍生類別的特定行為。 如果不呼叫基底類別實作,則衍生類別可自行決定是否要讓其行為與基底類別的行為相容。