多型通常是指物件導向程式設計的第三個重要部分,其重要性僅次於封裝和繼承。 多型在希臘文中表示「多種形狀」,可分成下列兩方面:
- 在執行階段,衍生類別的物件可以被視為基底類別的物件,例如方法參數和集合或陣列。 發生此多型時,物件的宣告型別與其執行階段型別將不再相同。
- 基類可能會定義和實現 虛擬方法,而衍生類可以 覆蓋 它們,這意味著它們提供自己的定義和實現。 在執行階段,當用戶端程式碼呼叫方法時,CLR 會查詢物件的執行階段類型,然後叫用虛擬方法的覆寫。 在原始程式碼中,您可以在基底類別上呼叫方法,然後執行衍生類別版本的方法。
虛擬方法可讓您以一致的方式來使用相關物件群組。 例如,假設您有一個繪圖應用程式,可讓使用者在繪圖介面上建立各種圖形。 您不知道使用者將在編譯時期建立哪一種圖形。 但是,應用程式必須追蹤所建立的所有不同圖形類型,並且必須根據使用者滑鼠動作來更新圖形。 您可以使用多型,分兩個基本步驟來解決這個問題:
- 建立類別階層架構,其中每個特定圖形類別都是衍生自一個通用基底類別。
- 使用虛擬方法,透過對基底類別方法發出單一呼叫,在任何衍生類別上叫用適當的方法。
首先,建立稱為 Shape 的基底類別,以及 Rectangle、Circle 和 Triangle 等衍生類別。 將稱為 Shape 的虛擬方法提供給 Draw 類別,然後在每個衍生類別中覆寫此方法,以繪製該類別代表的特定圖形。 建立 List<Shape> 物件,並將 Circle、Triangle 和 Rectangle 新增至其中。
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。
多型概觀
虛擬成員
當衍生類別繼承自基底類別時,其會包含基底類別的所有成員。 基底類別中宣告的所有行為都是衍生類別的一部分。 這可讓衍生類別的物件視為基底類別的物件。 存取修飾元 (public、 、 protectedprivate等) 會決定是否可從衍生類別實作存取這些成員。 虛擬方法使設計者能夠為衍生類別的行為提供不同的選擇。
- 衍生類別可以重寫基底類別中的虛擬成員,定義新的行為。
- 衍生類別可以繼承最接近的基底類別方法,而不覆寫它,保留現有的行為,但允許進一步的衍生類別覆寫該方法。
- 衍生類別可以定義隱藏基底類別實作之成員的新非虛擬實作。
只有在基底類別成員宣告為 virtual 或 abstract 時,衍生類別才能覆寫基底類別成員。 衍生成員必須使用 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 時。
防止衍生類別覆寫虛擬成員
虛擬成員始終保持虛擬狀態,無論在虛擬成員與最初宣告該成員的類別之間宣告了多少個類別。 如果 class A 宣告虛擬成員,且類別 B 衍生自 A,且類別 C 衍生自 B,則類別 C 會繼承虛擬成員,而且可能會覆寫它,而不論類別 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。 如果使用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();
}
}
如需詳細資訊,請參閱 base。
注意
建議虛擬成員在自己的實作中使用 base 來呼叫該成員的基底類別實作。 允許發生基底類別行為,可讓衍生類別集中實作衍生類別的特定行為。 如果未呼叫基類實作,則衍生類別可讓其行為與基類的行為相容。