泛型 (Generics)
泛型可用於針對不同的輸入資料型別定義函式與結構體。這項語言功能有時稱為參數化多型。在 Move 中,我們經常將泛型與 型別參數及 型別引數交替使用。
泛型常用於函式庫原始碼中,例如 vector,用來宣告可適用於任何可能型別(且符合指定限制)的原始碼。這類參數化讓你能在多種型別與情境中重複使用相同的實作。
宣告型別參數 (Declaring Type Parameters)
函式與結構都可以在其簽章中接受一串以角括號 <...> 包住的型別參數。
泛型函式 (Generic Functions)
函式的型別參數會放在函式名稱之後、(值)參數清單之前。以下程式碼定義了一個泛型識別函式,該函式接受任意型別的值,並原樣回傳該值。
fun id<T>(x: T): T {
// 此型別註解並非必要,但有效
(x: T)
}
定義後,型別參數 T 可用於參數型別、回傳型別,以及函式主體內部。
泛型結構 (Generic Structs)
結構的型別參數會放在結構名稱之後,並可用來命名欄位的型別。
public struct Foo<T> has copy, drop { x: T }
public struct Bar<T1, T2> has copy, drop {
x: T1,
y: vector<T2>,
}
請注意,不一定要使用型別參數
型別引數 (Type Arguments)
呼叫泛型函式 (Calling Generic Functions)
呼叫泛型函式時,可以在一對角括號所包圍的清單中,指定函式型別參數的型別引數。
fun foo() {
let x = id<bool>(true);
}
如果你未指定型別引數,Move 的型別推斷會為你提供它們。
使用泛型結構 (Using Generic Structs)
同樣地,建構或解構泛型型別的值時,可以附加結構型別參數的型別引數清單。
fun foo() {
// 建構時的型別引數
let foo = Foo<bool> { x: true };
let bar = Bar<u64, u8> { x: 0, y: vector<u8>[] };
// 解構時的型別引數
let Foo<bool> { x } = foo;
let Bar<u64, u8> { x, y } = bar;
}
無論如何,如果你未指定型別引數,Move 的型別推斷都會為你提供它們。
型別引數不相符 (Type Argument Mismatch)
如果你指定的型別引數與實際提供的值衝突,將會產生錯誤:
fun foo() {
let x = id<u64>(true); // 錯誤!true 不是 u64
}
同樣地:
fun foo() {
let foo = Foo<bool> { x: 0 }; // 錯誤!0 不是 bool
let Foo<address> { x } = foo; // 錯誤!bool 與 address 不相容
}
型別推斷 (Type Inference)
在大多數情況下,Move 編譯器能夠推斷型別引數,因此你不必明確寫下它們。若省略型別引數,上述範例會如下所示:
fun foo() {
let x = id(true);
// ^ 已推斷 <bool>
let foo = Foo { x: true };
// ^ 已推斷 <bool>
let Foo { x } = foo;
// ^ 已推斷 <bool>
}
請注意:當編譯器無法推斷型別時,你必須手動加上型別註記。常見情境是呼叫型別參數只出現在回傳位置的函式。
module a::m;
fun foo() {
let v = vector[]; // 錯誤!
// ^ 編譯器無法判斷元素型別,因為它從未被使用
let v = vector<u64>[];
// ^~~~~ 此情況必須手動加上型別註記。
}
請注意,這些情況有些刻意,因為 vector[] 從未被使用,因此 Move 的型別推斷無法推斷其型別。
不過,若該值稍後在函式中被使用,編譯器便能推斷其型別:
module a::m;
fun foo() {
let v = vector[];
// ^ 已推斷 <u64>
vector::push_back(&mut v, 42);
// ^ 已推斷 <u64>
}
「」型別 (_ Type) {#-type}
在某些情況下,你可能想明確註記部分型別引數,但讓編譯器推斷其他引數。_ 型別可作為這類預留位置,讓編譯器推斷型別。
let bar = Bar<u64, _> { x: 0, y: vector[b"hello"] };
// ^ 已推斷 vector<u8>
預留位置 _ 僅可出現在運算式與巨集函式定義中,不能出現在簽章中。這表示你無法將 _ 用於函式參數的定義、函式回傳型別、常數定義型別及資料型別欄位。
整數 (Integers)
在 Move 中,整數型別 u8、u16、u32、u64、u128 與 u256 全都是相異的型別。 不過,這些型別都可以使用相同的數值語法建立。換句話說,若未提供型別後綴,編譯器會根據數值的使用方式推斷整數型別。
let x8: u8 = 0;
let x16: u16 = 0;
let x32: u32 = 0;
let x64: u64 = 0;
let x128: u128 = 0;
let x256: u256 = 0;
若數值並未用於需要特定整數型別的內容中,預設會採用 u64。
let x = 0;
// ^ 預設使用 u64
不過,若數值對推斷出的型別而言過大,將會產生錯誤。
let i: u8 = 256; // 錯誤!
// ^^^ 對 u8 而言過大
let x = 340282366920938463463374607431768211454;
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 對 u64 而言過大
若數字過大,你可能需要明確標註其型別。
let x = 340282366920938463463374607431768211454u128;
// ^^^^ 有效!
未使用的型別參數 (Unused Type Parameters)
對於結構定義,未使用的型別參數是指未出現在該結構中任何已定義欄位內,但會在編譯時進行靜態檢查的型別參數。Move 允許未使用的型別參數,因此以下結構定義有效:
public struct Foo<T> {
foo: u64
}
這在為某些概念建立模型時可能很方便。以下是一個範例:
module a::m;
// 貨幣指定子
public struct A {}
public struct B {}
// 可使用貨幣指定子型別具現化的泛型 coin 型別。
// 例如:Coin<A>、Coin<B> 等。
public struct Coin<Currency> has store {
value: u64
}
// 以泛型方式撰寫關於所有貨幣的程式碼
public fun mint_generic<Currency>(value: u64): Coin<Currency> {
Coin { value }
}
// 以具體方式撰寫關於單一貨幣的程式碼
public fun mint_a(value: u64): Coin<A> {
mint_generic(value)
}
public fun mint_b(value: u64): Coin<B> {
mint_generic(value)
}
在此範例中,Coin<Currency> 會以 Currency 型別參數作為泛型,該參數指定 coin 的貨幣,並讓程式碼能以泛型方式處理任何貨幣,或以具體方式處理特定貨幣。即使 Currency 型別參數未出現在 Coin 中任何已定義的欄位內,這種通用性仍然適用。
幽靈型別參數 (Phantom Type Parameters)
在上述範例中,雖然 struct Coin 要求 store 能力,但 Coin<A> 與 Coin<B> 都不會具有 store 能力。這是因為條件式能力與泛型型別的規則,以及 A 和 B 沒有 store 能力;儘管它們甚至未在 struct Coin 的主體中使用。這可能造成一些不理想的後果。例如,我們無法將 Coin<A> 放入儲存空間中的錢包。
其中一個可行的解決方案是為 A 和 B 新增多餘的能力註記(亦即 public struct Currency1 has store {})。但這可能導致錯誤或安全性弱點,因為它會透過不必要的能力宣告弱化型別。例如,我們絕不會預期儲存空間中的值具有型別為 A 的欄位,但多餘的 store 能力會使此情況成為可能。此外,這些多餘註記會具有擴散性,要求許多對未使用型別參數泛型化的函式也納入必要的約束。
幽靈型別參數可解決此問題。未使用的型別參數可標記為 幽靈 型別參數,且不會參與結構體的能力推導。如此一來,在推導泛型型別的能力時,不會考量幽靈型別參數的引數,從而避免需要多餘的能力註記。為使這項寬鬆規則保持健全,Move 的型別系統保證宣告為 phantom 的參數,不是在結構體定義中完全未使用,就是僅作為同樣宣告為 phantom 的型別參數引數使用。
宣告 (Declaration)
在結構體定義中,可在型別參數宣告之前新增 phantom 關鍵字,將其宣告為幽靈型別參數。
public struct Coin<phantom Currency> has store {
value: u64
}
若型別參數宣告為 phantom,我們稱其為幽靈型別參數。定義結構體時,Move 的型別檢查器會確保每個幽靈型別參數不是未在結構體定義內使用,就是僅作為幽靈型別參數的引數使用。
public struct S1<phantom T1, T2> { f: u64 }
// ^^^^^^^ 有效,T1 未出現在結構體定義內
public struct S2<phantom T1, T2> { f: S1<T1, T2> }
// ^^^^^^^ 有效,T1 出現在幽靈位置
下列程式碼展示違反此規則的範例:
public struct S1<phantom T> { f: T }
// ^^^^^^^ 錯誤! ^ 並非幽靈位置
public struct S2<T> { f: T }
public struct S3<phantom T> { f: S2<T> }
// ^^^^^^^ 錯誤! ^ 並非幽靈位置
更正式地說,若型別作為幽靈型別參數的引數使用,我們稱該型別出現在 幽靈位置。有了此定義後,幽靈參數正確使用方式的規則可指定如下:幽靈型別參數只能出現在幽靈位置。
請注意,無須指定 phantom,但如果型別參數可以是 phantom 卻未如此標記,編譯器將發出警告。
具現化 (Instantiation)
具現化結構體時,推導結構體能力會排除幽靈參數的引數。例如,請考量下列程式碼:
public struct S<T1, phantom T2> has copy { f: T1 }
public struct NoCopy {}
public struct HasCopy has copy {}
現在考量型別 S<HasCopy, NoCopy>。由於 S 定義了 copy,且所有非幽靈引數都具有 copy,因此 S<HasCopy, NoCopy> 也具有 copy。
具有能力約束的幽靈型別參數 (Phantom Type Parameters with Ability Constraints)
能力約束與幽靈型別參數是正交功能,意即幽靈參數可透過能力約束宣告。
public struct S<phantom T: copy> {}
以具有能力約束的幽靈型別參數進行具現化時,型別引數仍必須滿足該約束,即使該參數是幽靈參數亦然。一般限制仍然適用,且 T 只能以具有 copy 的引數具現化。
約束條件 (Constraints)
在上述範例中,我們展示了如何使用型別參數來定義可由呼叫端在稍後代入的「未知」型別。然而,這表示型別系統掌握的型別資訊很少,必須以非常保守的方式執行檢查。從某種意義上來說,型別系統必須針對未受約束的泛型假設最糟情境——也就是沒有任何 能力 的型別。
約束條件可用來指定這些未知型別具備哪些特性,讓型別系統可以允許原本不安全的操作。
宣告約束條件 (Declaring Constraints)
可以使用下列語法對型別參數施加約束條件。
// T 是型別參數的名稱
T: <ability> (+ <ability>)*
<ability> 可以是四種能力中的任一種,且型別參數可以同時受多種能力約束。因此,下列所有型別參數宣告都有效:
T: copy
T: copy + drop
T: copy + drop + store + key
驗證約束條件 (Verifying Constraints)
約束條件會在具現化位置檢查。
public struct Foo<T: copy> { x: T }
public struct Bar { x: Foo<u8> }
// ^^ 有效,u8 具有 `copy`
public struct Baz<T> { x: Foo<T> }
// ^ 錯誤!T 沒有 'copy'
函式也是同樣的情況。
fun unsafe_consume<T>(x: T) {
// 錯誤!x 沒有 'drop'
}
fun consume<T: drop>(x: T) {
// 有效,x 會自動被丟棄
}
public struct NoAbilities {}
fun foo() {
let r = NoAbilities {};
consume<NoAbilities>(NoAbilities);
// ^^^^^^^^^^^ 錯誤!NoAbilities 沒有 'drop'
}
還有一些類似的範例,但使用的是 copy。
fun unsafe_double<T>(x: T) {
(copy x, x)
// 錯誤!T 沒有 'copy'
}
fun double<T: copy>(x: T) {
(copy x, x) // 有效,T 具有 'copy'
}
public struct NoAbilities {}
fun foo(): (NoAbilities, NoAbilities) {
let r = NoAbilities {};
double<NoAbilities>(r)
// ^ 錯誤!NoAbilities 沒有 'copy'
}
如需更多資訊,請參閱能力章節中的 條件式能力與泛型型別。
遞迴的限制 (Limitations on Recursions)
遞迴結構 (Recursive Structs)
泛型結構不能直接或間接包含相同型別的欄位,即使使用不同的型別引數也不行。以下所有結構宣告皆無效:
public struct Foo<T> {
x: Foo<u64> // 錯誤!'Foo' 包含 'Foo'
}
public struct Bar<T> {
x: Bar<T> // 錯誤!'Bar' 包含 'Bar'
}
// 錯誤!'A' 與 'B' 形成迴圈,同樣不被允許。
public struct A<T> {
x: B<T, u64>
}
public struct B<T1, T2> {
x: A<T1>
y: A<T2>
}
進階主題:型別層級遞迴 (Advanced Topic: Type-level Recursions)
Move 允許遞迴呼叫泛型函式。不過,與泛型結構結合使用時,這在某些情況下可能建立無限多種型別;允許這麼做會為編譯器、VM 與其他語言元件增加不必要的複雜性。因此,禁止此類遞迴。
未來可能會放寬這項限制,但目前以下範例應能讓你了解哪些做法被允許、哪些不被允許。
module a::m;
public struct A<T> {}
// 有限種型別 -- 允許。
// foo<T> -> foo<T> -> foo<T> -> ... 有效
fun foo<T>() {
foo<T>();
}
// 有限種型別 -- 允許。
// foo<T> -> foo<A<u64>> -> foo<A<u64>> -> ... 有效
fun foo<T>() {
foo<A<u64>>();
}
不被允許:
module a::m;
public struct A<T> {}
// 無限多種型別 -- 不允許。
// 錯誤!
// foo<T> -> foo<A<T>> -> foo<A<A<T>>> -> ...
fun foo<T>() {
foo<Foo<T>>();
}
同樣地,以下做法也不被允許:
module a::n;
public struct A<T> {}
// 無限多種型別 -- 不允許。
// 錯誤!
// foo<T1, T2> -> bar<T2, T1> -> foo<T2, A<T1>>
// -> bar<A<T1>, T2> -> foo<A<T1>, A<T2>>
// -> bar<A<T2>, A<T1>> -> foo<A<T2>, A<A<T1>>>
// -> ...
fun foo<T1, T2>() {
bar<T2, T1>();
}
fun bar<T1, T2> {
foo<T1, A<T2>>();
}
請注意,型別層級遞迴的檢查是根據對呼叫位置的保守分析,且不會將控制流程或執行階段值納入考量。
module a::m;
public struct A<T> {}
// 無限多種型別 -- 不允許。
// 錯誤!
fun foo<T>(n: u64) {
if (n > 0) foo<A<T>>(n - 1);
}
上述範例中的函式在技術上會針對任何給定輸入終止,因此只會建立有限多種型別;但 Move 的型別系統仍將其視為無效。