程式語言導論/泛型多型
泛型多型的符號可以承擔無限多種不同的型別。泛型多型主要分為兩種:引數多型和子型別多型。在本章的剩餘部分,我們將更詳細地瞭解這些變體。
引數多型是例程、名稱或符號的一個特性,這些例程、名稱或符號可以以一個或多個型別為引數。這種多型性使我們能夠定義通用的程式碼:它們可以被例項化以處理不同的型別。下面的程式碼展示了使用模板,這是在C++中實現引數多型的方式。
#include <iostream>
template <class T>
T GetMax (T a, T b) {
T result;
result = (a > b) ? a : b;
return (result);
}
int main() {
int i = 5, j = 6, k;
long l = 10, m = 5, n;
k = GetMax<int>(i, j); // type parameter: int
n = GetMax<long>(l, m); // type parameter: long
std::cout << k << std::endl;
std::cout << n << std::endl;
return 0;
}
上面的程式定義了一個名為GetMax(第 3 行到第 8 行)的多型函式。在GetMax的範圍內定義的型別變數T將在函式呼叫期間被實際型別替換。主函式展示了對GetMax的兩次呼叫。第 13 行的呼叫使用型別int,而第 14 行的呼叫使用型別long。GetMax的引數使用“>”運算子進行比較。因此,要使用此函式,必須將替換T的實際型別實現這種型別的比較。幸運的是,C++允許我們為自己的型別定義此運算子。例如,下面顯示的使用者定義類MyInt是GetMax的有效型別,因為它實現了大於運算子。
#include <iostream>
class MyInt {
friend std::ostream & operator<<(std::ostream& os, const MyInt& m) {
os << m.data;
}
friend bool operator >(MyInt& mi1, MyInt& mi2) {
return mi1.data > mi2.data;
}
public:
MyInt(int i) : data(i) {}
private:
const int data;
};
template <class T>
T GetMax (T a, T b) {
return (a > b) ? a : b;
}
int main () {
MyInt m1(50), m2(56);
MyInt mi = GetMax<MyInt>(m1, m2);
std::cout << mi << std::endl;
return 0;
}
引數多型存在於許多不同的靜態型別語言中。例如,下面在 Java 中實現的函式操作一個泛型型別的列表。請注意,儘管C++和 Java 的語法類似,但這些語言中的引數多型以不同的方式實現。在C++模板中,引數函式的每個例項都是單獨實現的。換句話說,C++編譯器為多型函式的每個特化生成一個全新的函式。Java的泛型只為每個引數化函式建立一個實現。
public static <E> void printList(List<E> l) {
for (E e : l) {
System.out.println(e);
}
}
SML以類似於Java的方式實現引數多型。在整個程式中,每個引數函式只有一個例項存在。這些函式操作值的引用,而不是值本身。例如,下面的函式計算SML中泛型列表的長度。請注意,我們的實現不需要了解儲存在列表中的值的任何資訊。它只操作此列表的結構,將儲存在其中的任何型別視為泛型引用。
- fun length nil = 0
= | length (_::t) = 1 + length t;
val length = fn : 'a list -> int
- length [1, 2, 3];
val it = 3 : int
- length [true, false, true];
val it = 3 : int
- length ["a", "bc", "def"];
val it = 3 : int

引數多型給了我們型別構造器的概念。型別構造器是一種接收型別併產生新型別的函式。例如,在上面的Java程式中,我們看到了型別構造器List<E>。我們不能用這種型別例項化Java物件。相反,我們需要使用它的特化,例如List<Integer>。因此,例如,用型別Integer例項化List<E>類似於將此型別傳遞給單引數函式List<E>,該函式返回List<Integer>。
引數多型是程式碼重用的重要機制。但是,並非每種程式語言都提供此功能。例如,廣泛使用的語言(如C、Fortran或Pascal)中不存在引數多型。但是,仍然可以使用幾種不同的策略來模擬它。例如,我們可以使用宏在C中模擬引數多型。下面的程式說明了此技術。宏SWAP有一個型別引數,類似於型別構造器。我們已經例項化了此宏兩次,第一次使用int,然後使用char*。
#include <stdio.h>
#define SWAP(T, X, Y) {T __aux = X; X = Y; Y = __aux;}
int main() {
int i0 = 0, i1 = 1;
char *c0 = "Hello, ", *c1 = "World!";
SWAP(int, i0, i1);
SWAP(char*, c0, c1);
printf("%d, %d\n", i0, i1);
printf("%s, %s\n", c0, c1);
}
在面嚮物件語言中存在一個眾所周知的特性,即Liskov替換原則。該原則指出,在賦值左側期望型別T的情況下,它也可以接收型別S,只要S是T的子型別。遵循Liskov替換原則的程式語言被認為提供了子型別多型。下面在Java中編寫的程式說明了這種多型性。這三個類,String, Integer和LinkedList是Object的子類。因此,函式print可以接收作為實際引數的任何這些三個類的例項物件。
import java.util.LinkedList;
public class Sub {
public static void print(Object o) {
System.out.println(o);
}
public static void main(String[] a) {
print(new String("dcc024"));
print(new Integer(42));
print(new LinkedList<Integer>());
}
}
子型別多型之所以有效,是因為如果S是T的子型別,則S滿足T所期望的契約。換句話說,型別T的任何屬性也存在於其子型別中S。在上面的示例中,函式print期望知道如何將自身轉換為字串的型別。在Java中,任何具有屬性toString()的型別都具有此知識。鑑於此屬性存在於類Object中,它也存在於根據語言語義為Object.

是S的子型別。S是T。例如,下面的程式碼說明了Java程式語言中子型別的鏈。在Java中,關鍵字extends用於確定一個類是另一個類的子型別。
class Animal {
public void eat() {
System.out.println(this + " is eating");
}
public String toString () {
return "Animal";
}
}
class Mammal extends Animal {
public void suckMilk() {
System.out.println(this + " is sucking");
}
public void eat() {
suckMilk();
}
}
class Dog extends Mammal {
public void bark() {
System.out.println(this + " is barking");
}
public String toString () {
return "Dog";
}
}
建立子型別關係的另一種機制是結構化子型別。這種策略不如名義子型別常見。最著名的採用結構化子型別的程式語言之一是 ocaml。下面用這種語言編寫的程式碼定義了兩個物件,x和y。請注意,即使這些物件沒有被顯式宣告為相同型別,但它們包含相同的介面,即它們都實現了方法get_x和set_x。因此,任何期望其中一個物件的程式碼都可以接收另一個物件。
let x =
object
val mutable x = 5
method get_x = x
method set_x y = x <- y
end;;
let y =
object
method get_x = 2
method set_x y = Printf.printf "%d\n" y
end;;
例如,函式let set_to_10 a = a#set_x 10;;可以接收x或y,例如,set_to_10 x和set_to_10 y是有效的呼叫。事實上,任何提供屬性set_x的物件都可以傳遞給set_to_10,即使該物件與x或y的介面不相同。我們用下面的程式碼說明了最後一條語句:換句話說,如果物件O提供了另一個物件P的所有屬性,那麼我們說O是P。請注意,程式設計師不需要顯式宣告此子型別關係。
let z =
object
method blahblah = 2.5
method set_x y = Printf.printf "%d\n" y
end;;
set_to_10 z;;
一般來說,如果S的子類,則T的子型別,則S包含比T更多的屬性。例如,在我們的類層次結構中,在Java中,Mammal的例項具有Animal的所有屬性,並且除了這些屬性之外,Mammal的例項還具有屬性suckMilk,該屬性不存在於Animal中。下圖說明了這一事實。該圖顯示,型別的屬性集是子型別屬性集的子集。

但是,超型別的例項比子型別的例項多。如果S的子類,則T,則S的每個例項也是T的例項,反之則不成立。下圖說明了此觀察結果。