泛型 (Generics)
Generics 是一種定義型別或函式的方式,讓它能與任何型別搭配運作,而不是綁定單一特定型別。你在本章中其實已經用過 generics,只是可能沒有注意到:vector 型別就是泛型的——單一定義即可容納任何型別的元素——Option 也是,它能包裝任何值。Generics 是集合、抽象實作以及 Move 許多進階功能的基礎。
Generics 解決的問題 (The Problem Generics Solve)
假設我們需要一個能包裝單一 u64 值的型別。很簡單:
public struct U64Container has drop {
value: u64,
}
但如果我們也需要包裝一個 bool 呢?還有 String?還有我們自己的 struct?每個版本除了 value 欄位的型別之外都完全相同,而每個處理容器的函式也都需要為每個版本重複撰寫一次:
public struct BoolContainer has drop { value: bool }
public struct StringContainer has drop { value: String }
// ...每個想儲存的型別都要一個新的 struct
Generics 正是為了解決這個問題:我們只定義容器一次,用一個佔位符取代具體型別,並在使用該型別時再填入實際型別。
Generic 語法 (Generic Syntax)
要定義泛型型別或函式,在名稱後面加上一組以角括號(< 和 >)括住的型別參數列表。多個型別參數以逗號分隔。
/// Container for any type `T`.
public struct Container<T> has drop {
value: T,
}
/// Function that creates a new `Container` with a generic value `T`.
public fun new<T>(value: T): Container<T> {
Container { value }
}
在上面的範例中,Container 是一個帶有單一型別參數 T 的泛型型別,容器的 value 欄位儲存型別為 T 的值。T 並不是真實的型別——它是一個佔位符,代表「某個型別,稍後再指定」。new 函式是帶有相同型別參數的泛型函式,它會回傳一個帶有給定值的 Container<T>。
依照慣例,型別參數以單一大寫字母命名——T、U、K、V。不過,也可以使用任何合法的名稱:例如標準函式庫就將 vector 的型別參數命名為 Element。
使用泛型型別 (Using Generic Types)
當我們建立泛型型別的實例時,佔位符會被替換為具體型別。每次替換都會產生一個不同的型別:Container<u8>、Container<bool> 與 Container<String> 雖然來自同一個定義,卻是三種不同的型別。
具體型別可以明確寫出,或在大多數情況下由編譯器推斷:
#[test]
fun test_container() {
// these three lines are equivalent
let container: Container<u8> = new(10); // type inference
let container = new<u8>(10); // create a new `Container` with a `u8` value
let container = new(10u8);
assert_eq!(container.value, 10);
// Value can be ignored only if it has the `drop` ability.
let Container { value: _ } = container;
}
測試的前三行是等價的——每一行都建立了一個 Container<u8>。因為數字字面值的型別是模糊的,我們必須在某處指定該數字的型別:在變數的型別標註中、在 new 的明確型別引數中,或是在字面值本身。只要給定其中一項,編譯器就能推斷出其餘部分。對於型別不模糊的值,例如 bool 或 String,則完全不需要任何標註。
多個型別參數 (Multiple Type Parameters)
型別或函式可以有多個以逗號分隔的型別參數:
/// A pair of values of any type `T` and `U`.
public struct Pair<T, U> {
first: T,
second: U,
}
/// Function that creates a new `Pair` with two generic values `T` and `U`.
public fun new_pair<T, U>(first: T, second: U): Pair<T, U> {
Pair { first, second }
}
在上面的範例中,Pair 是一個帶有兩個型別參數 T 和 U 的泛型型別,new_pair 函式會建立一個帶有給定值的 Pair。
#[test]
fun test_generic() {
// these three lines are equivalent
let pair_1: Pair<u8, bool> = new_pair(10, true); // type inference
let pair_2 = new_pair<u8, bool>(10, true); // create a new `Pair` with a `u8` and `bool` values
let pair_3 = new_pair(10u8, true);
assert_eq!(pair_1.first, 10);
assert_eq!(pair_1.second, true);
// Unpacking is identical.
let Pair { first: _, second: _ } = pair_1;
let Pair { first: _, second: _ } = pair_2;
let Pair { first: _, second: _ } = pair_3;
}
型別參數的順序很重要。Pair<u8, bool> 與 Pair<bool, u8> 是兩個不同、互不相容的型別——即使它們是由相同的定義建構出來,並儲存相同的資料:
#[test]
fun test_swap_type_params() {
let pair1: Pair<u8, bool> = new_pair(10u8, true);
let pair2: Pair<bool, u8> = new_pair(true, 10u8);
// this line will not compile
// assert_eq!(pair1, pair2);
let Pair { first: pf1, second: ps1 } = pair1; // first1: u8, second1: bool
let Pair { first: pf2, second: ps2 } = pair2; // first2: bool, second2: u8
assert_eq!(pf1, ps2); // 10 == 10
assert_eq!(ps1, pf2); // true == true
}
由於 pair1 與 pair2 的型別不同,pair1 == pair2 這樣的比較將無法編譯。這些值只能在拆解之後逐欄位比較。
為何使用 Generics? (Why Generics?)
到目前為止,我們專注於機制層面:如何定義泛型型別並建立其實例。Generics 真正的威力在於能夠只定義一次共用的資料與行為,並讓型別的一部分保持可變。考慮一個 User 型別,其中 name 與 age 欄位始終相同,但不同的應用程式需要附加不同的額外資料:
/// A user record with name, age, and some generic metadata
public struct User<T> {
name: String,
age: u8,
/// Varies depending on application.
metadata: T,
}
為 User<T> 定義的函式無論 metadata 是什麼型別都能運作——它們操作共用欄位,並不需要知道 T 的具體型別:
/// Updates the name of the user.
public fun update_name<T>(user: &mut User<T>, name: String) {
user.name = name;
}
/// Updates the age of the user.
public fun update_age<T>(user: &mut User<T>, age: u8) {
user.age = age;
}
#[test]
fun test_user() {
// In this instance, the `metadata` field is a `u64`...
let mut user1 = User {
name: "Alice",
age: 30,
metadata: 1000u64,
};
// ...and in this instance, it is a `bool`.
let mut user2 = User {
name: "Bob",
age: 40,
metadata: true,
};
// The same functions work for both instances.
user1.update_name("Alice II");
user2.update_name("Bob II");
assert_eq!(user1.name, "Alice II");
assert_eq!(user2.name, "Bob II");
let User { .. } = user1;
let User { .. } = user2;
}
在上面的測試中,一個 User 實例將 u64 儲存為其中繼資料,另一個則儲存 bool,但兩者都能透過同一個只定義一次的 update_name 函式來更新。
虛擬型別參數 (Phantom Type Parameters)
有時候,型別參數只需要作為一個標籤使用,而不需要儲存該型別的任何值。考慮一個 Coin 型別:實際資料只是一個數值 value,對每一種貨幣都相同。然而,一枚美元硬幣與一枚歐元硬幣絕不能混淆——在編譯器眼中它們應該是不同的型別。為了表達這一點,該型別參數會被宣告為 phantom——一個不出現在任何欄位中的參數:
/// A generic type with a phantom type parameter.
public struct Coin<phantom T> {
value: u64
}
Move 要求每個一般型別參數都必須用於 struct 的欄位中。由於 T 並未儲存在 Coin 的任何地方,它必須以 phantom 關鍵字標記。
貨幣接著可以被定義為空的 struct——它們不攜帶任何資料,存在的目的僅僅是作為標籤使用:
public struct USD {}
public struct EUR {}
#[test]
fun test_phantom_type() {
let coin1: Coin<USD> = Coin { value: 10 };
let coin2: Coin<EUR> = Coin { value: 20 };
// This line will not compile: `Coin<USD>` and `Coin<EUR>`
// are different types and cannot be mixed up.
// let mixed: Coin<USD> = coin2;
// Unpacking is identical because the phantom type parameter is not used.
let Coin { value: _ } = coin1;
let Coin { value: _ } = coin2;
}
即使 Coin<USD> 與 Coin<EUR> 儲存的資料完全相同,它們仍是不同的型別,而預期其中一種型別的函式將不會接受另一種型別。這種模式在實際應用中被廣泛使用:舉例來說,Sui Framework 中的 Coin 型別正是以這種方式定義的。
型別參數的約束 (Constraints on Type Parameters)
預設情況下,型別參數接受任何型別。然而,有時內部型別必須允許特定行為,例如可被複製或捨棄,為此可以將型別參數約束為具有特定能力 (abilities)。語法為 T: <ability> + <ability>:
/// A generic type with a type parameter that has the `drop` ability.
public struct Droppable<T: drop> {
value: T,
}
/// A generic struct with a type parameter that has the `copy` and `drop` abilities.
public struct CopyableDroppable<T: copy + drop> {
value: T, // T must have the `copy` and `drop` abilities
}
約束是具體型別必須遵守的承諾:Move 編譯器只允許用具有 drop 能力的型別來實例化 Droppable<T>,並且只允許用同時具有 copy 與 drop 能力的型別來實例化 CopyableDroppable<T>。不具備這些能力的型別將無法通過編譯:
/// Type without any abilities.
public struct NoAbilities {}
#[test]
fun test_constraints() {
// Fails - `NoAbilities` does not have the `drop` ability
// let droppable = Droppable<NoAbilities> { value: 10 };
// Fails - `NoAbilities` does not have the `copy` and `drop` abilities
// let copyable_droppable = CopyableDroppable<NoAbilities> { value: 10 };
}
延伸閱讀 (Further Reading)
- Move Reference 中的Generics。