跳轉到內容

程式語言導論/泛型多型

來自Wikibooks,開放世界中的開放書籍

泛型多型

[編輯 | 編輯原始碼]

泛型多型的符號可以承擔無限多種不同的型別。泛型多型主要分為兩種:引數多型和子型別多型。在本章的剩餘部分,我們將更詳細地瞭解這些變體。

引數多型

[編輯 | 編輯原始碼]

引數多型是例程、名稱或符號的一個特性,這些例程、名稱或符號可以以一個或多個型別為引數。這種多型性使我們能夠定義通用的程式碼:它們可以被例項化以處理不同的型別。下面的程式碼展示了使用模板,這是在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 行的呼叫使用型別longGetMax的引數使用“>”運算子進行比較。因此,要使用此函式,必須將替換T的實際型別實現這種型別的比較。幸運的是,C++允許我們為自己的型別定義此運算子。例如,下面顯示的使用者定義類MyIntGetMax的有效型別,因為它實現了大於運算子。

#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>

引數多型是程式碼重用的重要機制。但是,並非每種程式語言都提供此功能。例如,廣泛使用的語言(如CFortranPascal)中不存在引數多型。但是,仍然可以使用幾種不同的策略來模擬它。例如,我們可以使用宏在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,只要ST子型別。遵循Liskov替換原則程式語言被認為提供了子型別多型。下面在Java中編寫的程式說明了這種多型性。這三個類,String, IntegerLinkedListObject的子類。因此,函式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>());
  }
}

子型別多型之所以有效,是因為如果ST的子型別,則S滿足T所期望的契約。換句話說,型別T的任何屬性也存在於其子型別中S。在上面的示例中,函式print期望知道如何將自身轉換為字串的型別。在Java中,任何具有屬性toString()的型別都具有此知識。鑑於此屬性存在於類Object中,它也存在於根據語言語義為Object.

子型別的其他所有類中。Java 中一個簡單的類層次結構。在 Java 中,如果S是類T的子型別,則S的子類,則T.

S的子型別。ST。例如,下面的程式碼說明了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。下面用這種語言編寫的程式碼定義了兩個物件,xy。請注意,即使這些物件沒有被顯式宣告為相同型別,但它們包含相同的介面,即它們都實現了方法get_xset_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;;可以接收xy,例如,set_to_10 xset_to_10 y是有效的呼叫。事實上,任何提供屬性set_x的物件都可以傳遞給set_to_10,即使該物件與xy的介面不相同。我們用下面的程式碼說明了最後一條語句:換句話說,如果物件O提供了另一個物件P的所有屬性,那麼我們說OP。請注意,程式設計師不需要顯式宣告此子型別關係。

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的例項,反之則不成立。下圖說明了此觀察結果。

特設多型

華夏公益教科書