了解泛型
泛型对于 Move 语言是必不可少的,它使得 Move 语言在区块链世界中如此独特,它是 Move 灵活性的重要来源。
首先,让我来引用 Rust Book 对于泛型得定义:泛型是具体类型或其他属性的抽象替代品。实际上,泛型允许我们只编写单个函数,而该函数可以应用于任何类型。这种函数也被称为模板 —— 一个可以应用于任何类型的模板处理程序。
Move 中泛型可以应用于结构体和函数的定义中。
结构体中的泛型
首先,我们将创建一个可容纳u64
整型的 Box :
module book::generics {
public struct Box {
value: u64
}
}
这个 Box 只能包含u64
类型的值,这一点是非常清楚的。但是,如果我们想为u8
类型或 bool类型创建相同的 Box 该怎么办呢?分别创建u8
类型的 Box1 和bool
型 Box2 吗?答案是否定的,因为可以使用泛型。
module book::generics {
public struct Box<T> {
value: T
}
}
我们在结构体名字的后面增加<T>
。尖括号<..>
里面用来定义泛型,这里T
就是我们在结构体中模板化的类型。在结构体中,我们已经将T
用作常规类型。类型T实际并不存在,它只是任何类型的占位符。
函数中的泛型
现在让我们为上面的结构体创建一个构造函数,该构造函数将首先使用u64类型。
module book::generics {
public struct Box<T> {
value: T
}
// type u64 is put into angle brackets meaning
// that we're using Box with type u64
public fun create_box(value: u64): Box<u64> {
Box<u64>{ value }
}
}
带有泛型的结构体的创建稍微有些复杂,因为它们需要指定类型参数,需要把常规结构体 Box 变为 Boxcreate_box
方法更通用,有没有更简单的方法?有的,在函数中使用泛型!
module book::generics {
// ...
public fun create_box<T>(value: T): Box<T> {
Box<T> { value }
}
// we'll get to this a bit later, trust me
public fun value<T: copy>(box: &Box<T>): T {
*&box.value
}
}
函数调用中使用泛型
上例中在定义函数时,我们像结构体一样在函数名之后添加了尖括号。如何使用它呢?就是在函数调用中指定类型。
module book::generics {
use {{sender}}::storage;
use std::debug;
fun main() {
// value will be of type storage::Box<bool>
let bool_box = storage::create_box<bool>(true);
let bool_val = storage::value(&bool_box);
assert(bool_val, 0);
// we can do the same with integer
let u64_box = storage::create_box<u64>(1000000);
let _ = storage::value(&u64_box);
// let's do the same with another box!
let u64_box_in_box = storage::create_box<storage::Box<u64>>(u64_box);
// accessing value of this box in box will be tricky :)
// Box<u64> is a type and Box<Box<u64>> is also a type
let value: u64 = storage::value<u64>(
&storage::value<storage::Box<u64>>( // Box<u64> type
&u64_box_in_box // Box<Box<u64>> type
)
);
// you've already seed debug::print<T> method
// which also uses generics to print any type
debug::print<u64>(&value);
}
}
这里我们用三种类型使用了 Box:bool
, u64
和 Box<u64>
。最后一个看起来有些复杂,但是一旦你习惯了,并且理解了泛型是如何工作的,它成为你日常工作的好帮手。
继续下一步之前,让我们做一个简单的回顾。我们通过将泛型添加到Box
结构体中,使Box
变得抽象了。与 Box 能提供的功能相比,它的定义相当简单。现在,我们可以使用任何类型创建Box
,u64 或 address,甚至另一个 Box 或另一个结构体。
abilities 限制符
我们已经学习了 abilities,它们可以作为泛型的限制符来使用,限制符的名称和 ability 相同。
fun name<T: copy>() {} // allow only values that can be copied
fun name<T: copy + drop>() {} // values can be copied and dropped
fun name<T: key + store + drop + copy>() {} // all 4 abilities are present
也可以在结构体泛型参数中使用:
public struct name<T: copy + drop> { value: T } // T can be copied and dropped
public struct name<T: stored> { value: T } // T can be stored in global storage
请记住
+
这个语法符号,第一眼看上去可能不太适应,因为很少有语言在关键字列表中使用+
。
下面是一个使用限制符的例子:
module book::generics {
// contents of the box can be stored
public struct Box<T: store> has key, store {
content: T
}
}
另一个需要被提及的是结构体的成员必须和结构体具有相同的 abilities (除了key
以外)。这个很容易理解,如果结构体具有 copy ability,那么它的成员也必须能被 copy,否则结构体作为一个整体不能被 copy。Move 编译器允许代码不遵守这样的逻辑,但是运行时会出问题。
module book::generics {
// non-copyable or droppable struct
public struct Error {}
// constraints are not specified
public struct Box<T> has copy, drop {
contents: T
}
// this method creates box with non-copyable or droppable contents
public fun create_box(): Box<Error> {
Box { contents: Error {} }
}
}
这段代码可以成功编译和发布,但是如果你运行它就会出问题。
module book::generics {
fun main() {
{{sender}}::storage::create_box() // value is created and dropped
}
}
运行结果是报错 Box 不能被 drop。
┌── scripts/main.move:5:9 ───
│
5 │ storage::create_box();
│ ^^^^^^^^^^^^^^^^^^^^^ Cannot ignore values without the 'drop' ability. The value must be used
│
原因是创建结构体时所使用的成员值没有 drop ability。也就是 contents 不具备 Box 所要求的 abilities - copy 和 drop。
但是为了避免犯错,应该尽可能使泛型参数的限制符和结构体本身的 abilities 显式的保持一致。
所以下面这种定义的方法更安全:
// we add parent's constraints
// now inner type MUST be copyable and droppable
public struct Box<T: copy + drop> has copy, drop {
contents: T
}
泛型中包含多个类型
我们也可以在泛型中使用多个类型,像使用单个类型一样,把多个类型放在尖括号中,并用逗号分隔。我们来试着添加一个新类型Shelf
,它将容纳两个不同类型的Box
。
module book::generics {
public struct Box<T> {
value: T
}
public struct Shelf<T1, T2> {
box_1: Box<T1>,
box_2: Box<T2>
}
public fun create_shelf<Type1, Type2>(
box_1: Box<Type1>,
box_2: Box<Type2>
): Shelf<Type1, Type2> {
Shelf {
box_1,
box_2
}
}
}
Shelf
的类型参数需要与结构体字段定义中的类型顺序相匹配,而泛型中的类型参数的名称则无需相同,选择合适的名称即可。正是因为每种类型参数仅仅在其作用域范围内有效,所以无需使用相同的名字。
多类型泛型的使用与单类型泛型相同:
module book::generics {
use {{sender}}::storage;
fun main() {
let b1 = storage::create_box<u64>(100);
let b2 = storage::create_box<u64>(200);
// you can use any types - so same ones are also valid
let _ = storage::create_shelf<u64, u64>(b1, b2);
}
}
*你可以在函数或结构体定义中最多使用 18,446,744,073,709,551,615 (u64 最大值) 个泛型。你绝对不会达到此限制,因此可以随意使用。
未使用的类型参数
并非泛型中指定的每种类型参数都必须被使用。看这个例子:
module book::generics {
// these two types will be used to mark
// where box will be sent when it's taken from shelf
public struct Abroad {}
public struct Local {}
// modified Box will have target property
public struct Box<T, Destination> {
value: T
}
public fun create_box<T, Dest>(value: T): Box<T, Dest> {
Box { value }
}
}
也可以在脚本中使用 :
module book::generics {
use {{sender}}::storage;
fun main() {
// value will be of type storage::Box<bool>
let _ = storage::create_box<bool, storage::Abroad>(true);
let _ = storage::create_box<u64, storage::Abroad>(1000);
let _ = storage::create_box<u128, storage::Local>(1000);
let _ = storage::create_box<address, storage::Local>(0x1);
// or even u64 destination!
let _ = storage::create_box<address, u64>(0x1);
}
}