跳轉到內容

面向物件程式設計/多型性

來自華夏公益教科書

在程式語言和型別理論中,多型性是指為不同型別實體提供單個介面[1],或者使用單個符號來表示多個不同型別[2]。

最常識的重大多型性類別是

  • 特設多型性:為任意一組單獨指定的型別定義一個通用介面。
  • 引數多型性:當一個或多個型別沒有用名稱指定,而是用可以表示任何型別的抽象符號指定時。
  • 子型別化(也稱為子型別多型性包含多型性):當一個名稱表示由某個共同超類關聯的許多不同類的例項時[3]。

對多型型別系統的興趣在 1960 年代得到顯著發展,並在該十年末開始出現實際的實現。特設多型性引數多型性最初在 Christopher Strachey 的程式語言的基本概念[4] 中有所描述,在那裡它們被列為多型性的“兩個主要類別”。特設多型性是 Algol 68 的一個特徵,而引數多型性是 ML 型別系統的核心特徵。

在 1985 年的一篇論文中,Peter Wegner 和 Luca Cardelli 引入了包含多型性一詞來模擬子型別和繼承[2],並引用 Simula 作為第一個實現它的程式語言。

特設多型性

[編輯 | 編輯原始碼]

Christopher Strachey 選擇了特設多型性一詞來指代可以應用於不同型別引數的多型函式,但其行為取決於應用它們的實際引數的型別(也稱為函式過載或運算子過載)[5]。 在這種情況下,“特設”一詞並非貶義;它只是指這種多型性不是型別系統的一個基本特徵。 在下面的 Pascal / Delphi 示例中,Add 函式在檢視呼叫時似乎在各種型別上通用,但實際上編譯器將它們視為兩個完全獨立的函式,出於所有意圖和目的

program Adhoc;

function Add(x, y : Integer) : Integer;
begin
    Add := x + y
end;

function Add(s, t : String) : String;
begin
    Add := Concat(s, t)
end;

begin
    Writeln(Add(1, 2));                   (* Prints "3"             *)
    Writeln(Add('Hello, ', 'Mammals!'));    (* Prints "Hello, Mammals!" *)
end.

在動態型別語言中,情況可能更復雜,因為需要呼叫的正確函式可能只在執行時才能確定。

隱式型別轉換也被定義為一種多型性,稱為“強制轉換多型性”[2][6]。

引數多型性

[編輯 | 編輯原始碼]

引數多型性允許以通用方式編寫函式或資料型別,以便它可以統一處理值,而不依賴於其型別[7]。 引數多型性是使語言更具表現力的一種方式,同時仍然保持完全的靜態型別安全性。

引數多型性的概念同時適用於資料型別和函式。 一個可以評估為或應用於不同型別值的函式被稱為多型函式。 一個可以呈現為泛化型別(例如元素型別任意的列表)的資料型別被指定為多型資料型別,就像從其生成這種專門化的泛化型別一樣。

引數多型性在函數語言程式設計中無處不在,在那裡它通常簡稱為“多型性”。 下面的 Haskell 示例顯示了一個引數化的列表資料型別以及兩個引數化的多型函式

data List a = Nil | Cons a (List a)

length :: List a -> Integer
length Nil         = 0
length (Cons x xs) = 1 + length xs

map :: (a -> b) -> List a -> List b
map f Nil         = Nil
map f (Cons x xs) = Cons (f x) (map f xs)

引數多型性也存在於幾種面嚮物件語言中。 例如,C++ 和 D 中的模板,或者在 C# 和 Java 中稱為泛型

class List<T> {
    class Node<T> {
        T elem;
        Node<T> next;
    }
    Node<T> head;
    int length() { ... }
}

List<B> map(Func<A, B> f, List<A> xs) {
    ...
}

John C. Reynolds(以及後來的 Jean-Yves Girard)正式地將這種多型性概念發展為對 lambda 演算的擴充套件(稱為多型 lambda 演算或系統 F)。 任何引數化的多型函式必然在它可以做的事情方面受到限制,它作用於資料的形狀而不是它的值,從而導致引數性的概念。

子型別化

[編輯 | 編輯原始碼]

一些語言採用子型別化(也稱為子型別多型性包含多型性)來限制可以在多型性的特定情況下使用的型別的範圍。 在這些語言中,子型別化允許編寫一個函式來接受某個型別T的物件,但在傳遞屬於型別S的物件時也能正常工作,而型別ST的子型別(根據 Liskov 替換原則)。 這種型別關係有時寫成S <: T。 相反,T被稱為超型別S—寫成T :> S。 子型別多型性通常在動態解析(見下文)。

在下面的示例中,我們將貓和狗設為動物的子型別。 過程letsHear() 接受一個動物,但如果傳遞一個子型別給它,它也能正常工作

abstract class Animal {
    abstract String talk();
}

class Cat extends Animal {
    String talk() {
        return "Meow!";
    }
}

class Dog extends Animal {
    String talk() {
        return "Woof!";
    }
}

static void letsHear(final Animal a) {
    println(a.talk());
}

static void main(String[] args) {
    letsHear(new Cat());
    letsHear(new Dog());
}

在另一個例子中,如果NumberRationalInteger 是型別,使得Number :> RationalNumber :> Integer,則編寫用於接受Number 的函式在傳遞IntegerRational 時,與傳遞Number 時一樣有效。 物件的實際型別可以對客戶端隱藏在黑盒中,並透過物件標識訪問。 事實上,如果Number 型別是抽象的,甚至可能無法獲得其最派生型別為Number 的物件(參見抽象資料型別、抽象類)。 這種特定型別的型別層次結構被稱為——尤其是在 Scheme 程式語言的上下文中——數值塔,並且通常包含更多型別。

面向物件程式語言使用子類化(也稱為繼承)提供子型別多型性。 在典型的實現中,每個類包含一個所謂的虛擬表——一個實現類介面的多型部分的函式表——每個物件包含一個指向其類的“vtable”的指標,然後在每次呼叫多型方法時查詢該指標。 這種機制是以下內容的一個示例

  • 後期繫結,因為虛擬函式呼叫直到呼叫時才繫結;
  • 單分派(即單引數多型性),因為虛擬函式呼叫只是透過檢視第一個引數(this 物件)提供的 vtable 來繫結,因此其他引數的執行時型別完全無關。

大多數其他流行的物件系統也是如此。 但是,有些系統,比如 Common Lisp Object System,提供了多重分派,在多重分派下,方法呼叫在所有引數中都是多型的。

引數多型性和子型別化之間的相互作用導致了方差和限定量化的概念。

行多型性

[編輯 | 編輯原始碼]

行多型性[8] 是一個類似於子型別化但不同的概念。 它處理結構型別。 它允許使用所有型別具有某些屬性的值,而不會丟失剩餘的型別資訊。

多型別性

[編輯 | 編輯原始碼]

一個相關的概念是多型別性(或資料型別泛型)。 多型別函式比多型函式更通用,在這種函式中,“雖然可以為特定資料型別提供固定的特設情況,但特設組合器不存在”[9]。

實現方面

[編輯 | 編輯原始碼]

靜態和動態多型性

[編輯 | 編輯原始碼]

多型性可以透過實現選擇的時間來區分:靜態地(在編譯時)或動態地(在執行時,通常透過虛擬函式)。這分別被稱為靜態分派動態分派,相應的多型形式也相應地被稱為靜態多型動態多型

靜態多型執行速度更快,因為沒有動態分派開銷,但需要額外的編譯器支援。此外,靜態多型允許編譯器進行更深入的靜態分析(特別是為了最佳化)、原始碼分析工具和人類讀者(程式設計師)。動態多型更加靈活但速度更慢——例如,動態多型允許鴨子型別,動態連結庫可以操作物件而不知道它們的完整型別。

靜態多型通常發生在特設多型和引數化多型中,而動態多型在子型別多型中很常見。然而,透過更巧妙地使用模板超程式設計,特別是奇異遞迴模板模式,可以實現帶子型別的靜態多型。

華夏公益教科書