在之前介紹多型的文章中,我們曾經提到多載行為 (Overload),它容許我們在同一個函式中使用不同的資料型態跟數量的參數,讓函式可以有更多的變化,也可以應對更多的狀況。可是,有時侯我們會需要在函式中使用多載來處理不同資料型態的參數,但函式中執行內容卻是一樣的,像是下面的例子:[C#]
using System;
public class OverloadExample
{
public static int Add(int num1, int num2) { // 多載:第一個 Add 函式 - 兩個 int 參數
return num1 + num2;
}
public static string Add(string str1, string str2) { // 多載:第二個 Add 函式 - 兩個 string 參數
return str1 + str2;
}
public static void Main(string[] args)
{
Console.WriteLine(Add(1, 1)); // 由於有兩個 int 參數,因此使用了第一個 Add 函式,顯示: 2
Console.WriteLine(Add("1", "1")); // 由於有兩個 string 參數,因此使用了第二個 Add 函式,顯示: 11
}
}
在上面的例子中,我們可以看到使用 int 參數的函式 Add 跟使用 string 參數的函式 Add 的內容其實是完全一樣的。而考慮到函式 Add 的目的是把兩個參數加到一起,我們可以想像到在後續的更新中說不定需要準備其他資料型態的參數的函式 Add。難道我們需要把重複的內容按不同的資料型態重複編寫很多次嗎?因此,有部分的程式語言便提出了可以讓我們在同一個函式中支援我們放入不同的資料型態的方法,那就是模板/泛型 (Template/Generic)。
模板/泛型的概念是讓我們在函式中使用變數時不用指定它的資料型態而是暫時給它一個名稱,例如類型 T (源自模板英文單字中第一個字),把函式的內容決定好後在執行時才把變數指定為某種資料型態。在早期的程式語言如 C++ 中,它被稱為模板 (Template),不過後來被正式歸類為程式設計的一種風格或範式並被命名為泛型編程 (Generic Programming),因此較後期出現的程式語言都將其稱為泛型 (Generic)。不過,由於不是所有的程式語言都有支援模板/泛型,加上使用的方式在不同的程式語言會有一點不一樣,因此大家在使用前建議可以先查詢該程式語言的官方文檔。常見會使用泛型的程式語言有:C++、Ada、Delphi、Eiffel、Java、C#、F#、Swift 和 Visual Basic .NET 等。
例如我們把上面的函式 Add 改為泛型的函式:[C#]
using System;
using System.Collections.Generic;
public class GenericExample
{
public static T Add<T>(T var1, T var2) { // 泛型 Add 函式 - 兩個相同資料型態的參數,我們暫時把未知的資料型態命名為類型 T,讓程式知道我們的兩個參數跟回傳的資料型態都是一樣的
dynamic dynamic1 = var1; // 把參數 var1 轉換為可以進行加法運算式的 dynamic 類型
dynamic dynamic2 = var2; // 把參數 var2 轉換為可以進行加法運算式的 dynamic 類型
return dynamic1 + dynamic2; // 回傳兩者相加的結果
}
public static void Main(string[] args)
{
Console.WriteLine(Add<int>(1, 1)); // 把類型 T 指定為 int,顯示: 2
Console.WriteLine(Add<string>("1", "1")); // 把類型 T 指定為 string,顯示: 11
Console.WriteLine(Add<float>(1.2f, 1.2f)); // 把類型 T 指定為 float,顯示: 2.4
}
}
我們可以看到,透過使用 <>
宣告函式支援泛型及命名未知的資料型態後,我們可以讓函式使用同樣的程式碼來支援不同類型的資料型態。不過,需要注意的是,由於不同的資料型態都會有不同的特性,因此我們很容易在泛型的處理上出現錯誤,例如把無法進行加法運算式的資料型態放到上面的泛型 Add 函式中:
Add<bool>(true, true); // 錯誤! bool 資料型態無法進行加法運算式!
即便如此,泛型還是非常方便的,它可以用來暫時代表物件的類別,這在我們需要為複數的類別進行同樣的事情時非常有用。另一方面,我們也可以為暫時類型加上條件來限制它可以代表的資料型態或類別的範圍,例如:[C#]
using System;
using System.Collections.Generic;
// 父類別
public class Bird {
public string name;
public void speak() {
Console.WriteLine(name + " Quack!");
}
public virtual void move() { // 把 Duck 類別跟 Eagle 類別的移動行為整合在一起
Console.WriteLine(name + " Move!");
}
}
public class Duck : Bird { // 繼承 Bird 類別
public Duck() { // 建構子
name = "Duck";
}
public override void move() { // 把 move 函式覆寫為 Duck 類別獨有的版本
Console.WriteLine(name + " Swim!");
}
}
public class Eagle : Bird { // 繼承 Bird 類別
public Eagle() { // 建構子
name = "Eagle";
}
public override void move() { // 把 move 函式覆寫為 Eagle 類別獨有的版本
Console.WriteLine(name + " Fly!");
}
}
// 我們準備了一個沒有繼承 Bird 類別但內容一樣的 OtherBird 類別
public class OtherBird {
public string name;
public OtherBird() { // 建構子
name = "Other Bird";
}
public void speak() {
Console.WriteLine(name + " Quack!");
}
public virtual void move() {
Console.WriteLine(name + " Move!");
}
}
public class GenericExample2
{
public static void birdAction<T>(T newBird) where T : Bird { // 泛型函式,並加上條件指定類別 T 必須繼承 Bird 類別
newBird.speak(); // 由於我們知道類型 T 必須繼承 Bird 類別,因此可以使用 Bird 類別的 speak 函式
newBird.move(); // 由於我們知道類型 T 必須繼承 Bird 類別,因此可以使用 Bird 類別的 move 函式
}
public static void Main(string[] args)
{
birdAction<Eagle>(new Eagle()); // 顯示:Eagle Quack!, Eagle Fly!
birdAction<Duck>(new Duck()); // 顯示:Duck Quack!, Duck Fly!
birdAction<OtherBird>(new OtherBird()); // 錯誤! 即使 OtherBird 類別也有 speak 跟 move 函式,它並不乎合泛型 birdAction 函式的類別 T 必須繼承 Bird 類別的條件
}
}