Move语言 泛型
泛型对于 Move 语言是必不可少的,它是 Move 灵活性的重要来源。
泛型是具体类型或其他属性的抽象替代品。实际上,泛型允许我们只编写单个函数,而该函数可以应用于任何类型。
这种函数也被称为模板 —— 一个可以应用于任何类型的模板处理程序。
Move 中泛型可以应用于结构体和函数的定义中。
结构体中的泛型
首先,我们将创建一个可容纳u64整型的 Box :
module Storage {
struct Box {
value: u64
}
}
这个 Box 只能包含u64类型的值,这一点是非常清楚的。但是,如果我们想为u8类型或 bool类型创建相同的 Box 该怎么办呢?分别创建u8类型的 Box1 和bool型 Box2 吗?答案是否定的,因为可以使用泛型。
module Storage {
struct Box<T> {
value: T
}
}
我们在结构体名字的后面增加<T>。尖括号<..>里面用来定义泛型,这里T就是我们在结构体中模板化的类型。在结构体中,我们已经将T用作常规类型。类型T实际并不存在,它只是任何类型的占位符。
函数中的泛型
现在让我们为上面的结构体创建一个构造函数,该构造函数将首先使用u64类型。
module Storage {
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 变为 Box。Move没有任何限制什么类型可以被放进尖括号中。但是为了让create_box方法更通用,有没有更简单的方法?有的,在函数中使用泛型!
module Storage {
// ...
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
}
}
函数调用中使用泛型
上例中在定义函数时,我们像结构体一样在函数名之后添加了尖括号。如何使用它呢?就是在函数调用中指定类型。
script {
use {{sender}}::Storage;
use 0x1::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
也可以在结构体泛型参数中使用:
struct name<T: copy + drop> { value: T } // T can be copied and dropped
struct name<T: stored> { value: T } // T can be stored in global storage
请记住 + 这个语法符号,第一眼看上去可能不太适应,因为很少有语言在关键字列表中使用 +。
下面是一个使用限制符的例子:
module Storage {
// contents of the box can be stored
struct Box<T: store> has key, store {
content: T
}
}
另一个需要被提及的是结构体的成员必须和结构体具有相同的 abilities (除了key以外)。这个很容易理解,如果结构体具有 copy ability,那么它的成员也必须能被 copy,否则结构体作为一个整体不能被 copy。Move 编译器允许代码不遵守这样的逻辑,但是运行时会出问题。
module Storage {
// non-copyable or droppable struct
struct Error {}
// constraints are not specified
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 {} }
}
}
这段代码可以成功编译和发布,但是如果你运行它就会出问题。
script {
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
struct Box<T: copy + drop> has copy, drop {
contents: T
}
泛型中包含多个类型
我们也可以在泛型中使用多个类型,像使用单个类型一样,把多个类型放在尖括号中,并用逗号分隔。我们来试着添加一个新类型Shelf,它将容纳两个不同类型的Box。
module Storage {
struct Box<T> {
value: T
}
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的类型参数需要与结构体字段定义中的类型顺序相匹配,而泛型中的类型参数的名称则无需相同,选择合适的名称即可。正是因为每种类型参数仅仅在其作用域范围内有效,所以无需使用相同的名字。
多类型泛型的使用与单类型泛型相同:
script {
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 Storage {
// these two types will be used to mark
// where box will be sent when it's taken from shelf
struct Abroad {}
struct Local {}
// modified Box will have target property
struct Box<T, Destination> {
value: T
}
public fun create_box<T, Dest>(value: T): Box<T, Dest> {
Box { value }
}
}
也可以在脚本中使用 :
script {
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);
}
}
在这里,我们使用泛型标记类型,但实际上并没有真正使用它。当你了解resource概念后,就会知道为什么这种定义很重要。目前,就当这只是使用泛型的一种方法。
下一章:Move语言 数组 Vector
我们能够使用 struct 创建自己的类型并存储复杂数据。但是有时我们需要动态、可扩展和可管理的功能。为此,Move 提供了向量 Vector。Vector 是用于存储数据集合的内置类型。集合的数据可以是任 ...
AI 中文社