Move 是 Diem 项目 专门为区块链开发的一种安全可靠的智能合约编程语言。
与其它智能合约编程语言(例如 Solidity)不同,Move 程序分为脚本和模块。前者可以让开发者在交易中加入更多逻辑,在更加灵活地同时节省时间和资源。后者允许开发人员更容易扩展区块链的功能,更加灵活地实现自定义智能合约。
Move 的基本数据类型包括: 整型 (u8, u64, u128)、布尔型 boolean 和地址 address
Move 不支持字符串和浮点数
当需要比较值的大小或者当函数需要输入不同大小的整型参数时,你可以使用as运算符将一种整型转换成另外一种整型
可以使用双斜杠“//”编写行注释
块注释以"/"开头,并包含第一个"/"之前的所有文本
表达式是具有返回值的代码单元。有返回值的函数调用是一个表达式,它有返回值;整型常数也是一个表达式,它返回整数等
表达式必须用分号";"隔开
空表达式
文字表达式
块表达式用花括号"{}"表示。块可以包含其它表达式(和其它代码块)。函数体在某种意义上也是一个代码块。
代码块可以返回一个值,如果它后面没有分号,则返回值为代码块内最后一个表达式的值。
script {
use 0x1::Debug;
fun block_ret_sample() {
// since block is an expression, we can
// assign it's value to variable with let
let a = {
let c = 10;
c * 1000 // 没有分号
}; // 作用域结束, 变量获得值为10000
let b = {
a * 1000 // 没有分号
};
// 变量获得值为 10000000
{
10; // 有分号
}; // 这个块没有返回值
// let _ = a + b;
Debug::print(&(a+b)); // 10010000
}
}
常量可以定义为基本类型(比如整数,布尔值和地址),也可以定义为数组。
我们可以通过名称访问常量,常量模块是本地可见的。
使用const声明
作用域是绑定生效的代码区域。
Move 作用域是由花括号扩起来的代码块,它本质上是一个块。
使用let声明变量,用来创建一个新变量,该变量要么为空(未定义),要么为某表达式的值。该表达式的语法是:let : ;或let = 。
声明时可指定类型,也可省略,由编译器推断
创建和初始化变量后,就可以使用变量名来修改或访问它所代表的值了
Move 中每个变量都必须被使用,否则代码编译不会通过, 因此我们不能初始化一个变量却不去使用它。但是你可以用下划线来告诉编译器,这个变量是故意不被使用的。
Move 允许两次定义同一个的变量,第一个变量将会被屏蔽。也称move了
生命周期和可见性:
变量仅存在于其作用域(或代码块)内,当作用域结束时变量随之消亡
作用域之外不可访问
Move具有多种用于修改整数值的运算符:

代码运行的顺序,Move支持if表达式和循环表达式。
顺序执行
分支控制
if (<布尔表达式>) <表达式> else <表达式>;
if是一个表达式,我们可以在 let 声明中使用它。但是像所有其他表达式一样,它必须以分号结尾。
script { use 0x1::Debug; fun main() { let a = true; let b = if (a) { 10 } else { 20 }; Debug::print<u8>(&b); // 10 } }循环控制
while 条件循环 while (<布尔表达式>) <表达式>; 与 if 表达式不同的是,while 表达式没有返回值,因而也就不能像 if 那样把自己赋值给某变量。
loop 无限循环 Move 提供了一种定义无限循环的方法,它没有条件判断,会一直执行。 一旦执行该代码将消耗所有给定资源(交易费),大多数情况下,编译器也无法判断循环是否是无限的,也就无法阻止无限循环代码的发布。 因此,使用无限循环时一定要注意安全,通常情况下建议使用 while 条件循环。
script { fun main() { let i = 0; loop { i = i + 1; }; // 无法访问的代码 let _ = i; } }通过 continue 和 break 控制循环
continue 和 break 关键字,分别允许程序跳过一轮循环或中断循环,可以在两种类型的循环中同时使用它们。
script { fun main() { let i = 0; loop { i = i + 1; if (i / 2 == 0) continue; if (i == 5) break; // do something }; 0x1::Debug::print<u8>(&i); } }如果 break 和 continue 是代码块中的最后一个关键字,则不能在其后加分号,因为后面的任何代码都不会被执行。
script { fun main() { let i = 0; loop { i = i + 1; if (i == 5) { break; // will result in compiler error. correct is `break` without semi // Error: Unreachable code }; // same with continue here: no semi, never; if (true) { continue }; // however you can put semi like this, because continue and break here // are single expressions, hence they "end their own scope" if (true) continue; if (i == 5) break; } } }
有条件退出 abort 有时,当某些条件失败时,您需要中止程序的执行。对于这种情况,Move 提供了有键字 abort,允许程序中止执行的同时报告错误代码。
script {
fun main(a: u8) {
if (a != 10) {
abort 0;
}
// code here won't be executed if a != 10
// transaction aborted
}
}
使用assert内置方法,assert(,
)对 abort和条件进行了封装,你可以在代码中任何地方使用它。assert() 在不满足条件时将中止执行,在满足条件时将不执行任何操作。script { fun main(a: u8) { assert(a == 10, 0); // code here will be executed if (a == 10) } }模块是发布在特定地址下的打包在一起的一组函数和结构体
模块在发布者的地址下发布。标准库在 0x1 地址下发布
发布模块时,不会执行任何函数。要使用模块就得使用脚本
模块以module关键字开头,后面跟随模块名称和大括号,大括号中放置模块内容。
module Math { // module contents public fun sum(a: u64, b: u64): u64 { a + b } }默认情况下,模块将在发布者的地址下进行编译和发布。但如果只是测试或开发,或者想要在模块中指定地址,请使用以下address {}语法:
address 0x1 { module Math { // module contents public fun sum(a: u64, b: u64): u64 { a + b } } }Move 在默认上下文中只能使用基本类型,也就是整型、布尔型和地址,可以执行的有意义或有用的操作也就是操作这些基本类型,或者基于基本类型定义新的类型。
除此之外还可以导入已发布的模块(或标准库)。
直接导入
可以直接在代码中按其地址使用模块:
script { fun main(a: u8) { 0x1::Debug::print(&a); } }使用关键字use。要使代码更简洁(注意,0x1是特殊的地址,实际地址是很长的),可以使用关键字use:
use <Address>::<ModuleName>;Address: 模块发布者的地址 ModuleName: 模块的名字
要访问导入的模块的方法(或类型),需要使用::符号
script { use 0x1::Vector; fun main() { // here we use method empty() of module Vector // the same way we'd access any other method of any other module let _ = Vector::empty<u64>(); } }在脚本中,模块导入必须放在 script {} 块内:
script { use 0x1::Vector; // in just the same way you can import any // other module(s). as many as you want! fun main() { let _ = Vector::empty<u64>(); } }在模块中导入模块必须在 module {} 块内进行:
module Math { use 0x1::Vector; // the same way as in scripts // you are free to import any number of modules public fun empty_vec(): vector<u64> { Vector::empty<u64>(); } }导入语句还可以进一步被扩展,可以直接导入模块的成员:
script { // single member import use 0x1::Signer::address_of; // multi member import (mind braces) use 0x1::Vector::{ empty, push_back }; fun main(acc: &signer) { // use functions without module access let vec = empty<u8>(); push_back(&mut vec, 10); // same here let _ = address_of(acc); } }script { use 0x1::Vector::{ Self, // Self == Imported module empty }; fun main() { // `empty` imported as `empty` let vec = empty<u8>(); // Self means Vector Vector::push_back(&mut vec, 10); } }当两个或多个模块具有相同的名称时,可以使用关键字as更改导入的模块的名称,这样可以在解决命名冲突的同时缩短代码长度。 语法:
use <Address>::<ModuleName> as <Alias>;脚本中的例子:
script { use 0x1::Vector::{ Self as V, empty as empty_vec }; fun main() { // `empty` imported as `empty_vec` let vec = empty_vec<u8>(); // Self as V = Vector V::push_back(&mut vec, 10); } }在模块中:
module Math { use 0x1::Vector as Vec; fun length(&v: vector<u8>): u64 { Vec::length(&v) } }Move 中代码的执行是通过调用函数实现的。函数以 fun 关键字开头,后跟函数名称、扩在括号中的参数,以及扩在花括号中的函数体。
Move 函数使用snake_case命名规则,也就是小写字母以及下划线作为单词分隔符。
fun function_name(arg1: u64, arg2: bool): u64 { // function body }定义模块时,你可能希望其他开发人员可以访问某些函数,而某些函数则保持隐藏状态。这正是函数可见性修饰符发挥作用的时候。
默认情况下,模块中定义的每个函数都是私有的,无法在其它模块或脚本中访问。
私有函数只能在定义它们的模块中访问,可用来执行一些内部工作。
我们在模块中定义的某些函数前加关键字public,表示该函数可公开访问。
脚本块只能包含一个被视为 main 的函数。它作为交易被执行,可以有参数,但是没有返回值。它可以操作其它已经发布的模块中的函数。
script { use 0x1::Account; fun main(addr: address) { assert(Account::exists(addr), 1); } }注意:由于只有一个函数,因此你可以按任意方式对它命名。一般情况下我们遵循惯用的编程概念将其称为 main。
脚本中能使用的函数功能是相对有限的,函数的全部潜能只有在模块中才能展现。 模块是一组函数和结构体,它可以封装一项或多项功能。
module Math { public fun sum(a: u64, b: u64): u64 { a + b } fun zero(): u8 { 0 } }像作用域中定义的任何其他变量一样,函数参数仅存在于函数体内。当函数块结束时,参数也会消亡。
函数可以根据需要接受任意多个参数(传递给函数的值)。
每个参数都有两个属性:参数名,也就是参数在函数体内的名称,以及参数类型。参数必须具有类型,并且必须用逗号分隔.
函数可以有多个返回值,返回值放在括号后,并且必须在冒号后面
module Math { // ... public fun max(a: u8, b: u8): (u8, bool) { if (a > b) { (a, false) } else if (a < b) { (b, false) } else { (a, true) } } }现在让我们看看如何在另一个脚本中使用该函数的返回值。
script { use 0x1::Debug; use 0x1::Math; fun main(a: u8, b: u8) { let (max, is_equal) = Math::max(99, 100); assert(is_equal, 1); Debug::print<u8>(&max); } }上面例子中,我们解构了一个二元组,用函数 max 的返回值创建了两个新变量。返回值的顺序保持不变,变量 max 用来存储 u8 类型的最大值,而 is_equal 用来存储 bool 类型。
返回值数量并没有限制,你可以根据需要决定元组的元素个数。还有一种返回复杂数据的方法,那就是结构体。
关键字 return 允许函数结束执行并返回结果。它可以与 if 条件一起使用,这样可以根据条件返回不同结果。
module M { public fun conditional_return(a: u8): bool { if (a == 10) { return true // semi is not put! }; if (a < 10) { true } else { false } } }有一种特殊的函数叫做"本地方法"。
本地方法实现的功能超出了 Move 的能力,它可以提供了额外的功能。
本地方法由 VM 本身定义,并且在不同的VM实现中可能会有所不同。
这意味着它们没有用 Move 语法实现,没有函数体,直接以分号结尾。
关键字 native 用于标记本地函数,它和函数可见性修饰符不冲突,native 和 public 可以同时使用。
这是 Diem 标准库中的示例:
module Signer { native public fun borrow_address(s: &signer): &address; // ... some other functions ... }结构体使用关键字 struct 定义。
结构体是自定义类型,它可以包含复杂数据,也可以不包含任何数据。
结构体由字段组成,可以简单地理解成"key-value"存储,其中 key 是字段的名称,而 value 是存储的内容。
结构体是在 Move 中创建自定义类型的唯一方法。
一个结构体最多可以有 65535 个字段。
被定义的结构体会成为新的类型,可以通过定义它的模块访问此类型,用法 M::MyStruct。、
Move 允许使用其它结构作为成员,但不能递归使用相同的结构体。
结构体只能在模块内部定义,并且以关键字 struct 开头 语法:
struct NAME { FIELD1: TYPE1, FIELD2: TYPE2, ... }例子:
module M { // struct can be without fields // but it is a new type struct Empty {} struct MyStruct { field1: address, field2: bool, field3: Empty } struct Example { field1: u8, field2: address, field3: u64, field4: bool, field5: bool, // you can use another struct as type field6: MyStruct } }要使用某结构体类型,需要先创建其实例。 要创建一个空结构体(没有字段),只需使用花括号. 可以用结构体的定义来创建实例,不同的是传入具体的值而不是类型。
module Country { struct Country { id: u8, population: u64 } // Contry is a return type of this function! public fun new_country(c_id: u8, c_population: u64): Country { // structure creation is an expression let country = Country { id: c_id, population: c_population }; country } }还可以通过传递与结构体的字段名匹配的变量名来简化创建新实例的代码。下面的 new_country() 函数中使用了这个简化方法:
// ... public fun new_country(id: u8, population: u64): Country { // id matches id: u8 field // population matches population field Country { id, population } // or even in one line: Country { id, population } }只有在模块内才可以访问其结构体的字段。在模块之外,该结构体字段是不可见的。
在此模块之外(在脚本或其他模块中),它只是一种类型.
使用"."符号访问结构的字段。
// ... public fun get_country_population(country: Country): u64 { country.population // <struct>.<property> }为了使结构体字段在外部可读,需要实现一些方法,这些方法将读取这些字段并将它们作为返回值传递。通常,getter 方法的调用方式与结构体的字段相同。
module Country { struct Country { id: u8, population: u64 } public fun new_country(id: u8, population: u64): Country { Country { id, population } } // don't forget to make these methods public! public fun id(country: &Country): u8 { country.id } // don't mind ampersand here for now. you'll learn why it's // put here in references chapter public fun population(country: &Country): u64 { country.population } // ... fun destroy ... }通过 getter 方法,我们允许模块的使用者访问结构体的字段:
script { use {{sender}}::Country as C; use 0x1::Debug; fun main() { // variable here is of type C::Country let country = C::new_country(1, 10000000); Debug::print<u8>( &C::id(&country) ); // print id Debug::print<u64>( &C::population(&country) ); // however this is impossible and will lead to compile error // let id = country.id; // let population = country.population. } }解构、或者销毁结构体需要使用以下所示语法:
module Country { // ... // we'll return values of this struct outside public fun destroy(country: Country): (u8, u64) { // variables must match struct fields // all struct fields must be specified let Country { id, population } = country; // after destruction country is dropped // but its fields are now variables and // can be used (id, population) } }请注意,Move 中禁止定义不会被使用的变量,有时你可能需要在不使用其字段的情况下销毁该结构体。对于未使用的结构体字段,用下划线"_"表示:
module Country { // ... public fun destroy(country: Country) { // this way you destroy struct and don't create unused variables let Country { id: _, population: _ } = country; // or take only id and don't init `population` variable // let Country { id, population: _ } = country; } }销毁结构体并不是必需的,但是,Resource结构体则必需被销毁。
Move 的类型系统非常灵活,每种类型都可以被四种限制符所修饰。这四种限制符我们称之为 abilities,它们定义了类型的值是否可以被复制、丢弃和存储。
key-被修饰的值可以作为键值对全局状态进行访问。
store-被修饰的值可以被存储到全局状态。
drop-被修饰的值在作用域结束时可以被丢弃。
copy-被修饰的值可以被复制。
基本类型和内建类型的 abilities 是预先定义好的并且不可改变: integers, vector, addresses 和 boolean 类型的值先天具有 copy,drop 和 store ability。
结构体的 ability 可以由开发者按照下面的语法进行添加:
struct NAME has ABILITY [, ABILITY] { [FIELDS] }例子:
module Library { // each ability has matching keyword // multiple abilities are listed with comma struct Book has store, copy, drop { year: u64 } // single ability is also possible struct Storage has key { books: vector<Book> } // this one has no abilities struct Empty {} }address 0x1{ module Country { struct Country { id: u8, population: u64 } public fun new_country(id: u8, population: u64): Country { Country { id, population } } } } script { use 0x1::Country; fun main() { Country::new_country(1, 1000000); } }运行上面的代码会报如下错误:
error: ┌── scripts/main.move:5:9 ─── │ 5 │ Country::new_country(1, 1000000); │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Cannot ignore values without the 'drop' ability. The value must be used │方法 Country::new_country() 创建了一个值,这个值没有被传递到任何其它地方,所以它应该在函数结束时被丢弃。但是 Country 类型没有 Drop ability,所以运行时报错了。
address 0x1{ module Country { struct Country has drop { id: u8, population: u64 } // ... } }注意 Destructuring 并不需要 Drop ability。
Move VM 实现了类似 Rust 的所有权功能。关于所有权的详细描述,可以参考 Rust Book 。
每个变量只有一个所有者作用域。当所有者作用域结束时,变量将被删除。
所有者是拥有某变量的作用域。变量可以在作用域内定义(例如,在脚本中使用关键字 let),也可以作为参数传递给作用域。由于 Move 中唯一的作用域是函数的作用域,所以除了这两种方法,没有其它方法可以将变量放入作用域。 每个变量只有一个所有者,这意味着当把变量作为参数传递给函数时,该函数将成为新所有者,并且第一个函数不再拥有该变量。或者可以说,第二个函数接管了变量的所有权。
script { use {{sender}}::M; fun main() { // Module::T is a struct let a : Module::T = Module::create(10); // here variable `a` leaves scope of `main` function // and is being put into new scope of `M::value` function M::value(a); // variable a no longer exists in this scope // this code won't compile M::value(a); } }module M { // create_fun skipped struct T { value: u8 } public fun create(value: u8): T { T { value } } // variable t of type M::T passed // `value()` function takes ownership public fun value(t: T): u8 { // we can use t as variable t.value } // function scope ends, t dropped, only u8 result returned // t no longer exists }我们可以看到,当函数 value() 结束时,t 将不复存在,返回的只是一个 u8 类型的值。 如何让t仍然可用呢?当然,一种快速的解决方案是返回一个元组,该元组包含原始变量和其它结果,但是 Move 还有一个更好的解决方案。
首先,我们了解一下 Move VM 的工作原理,以及将值传递给函数时会发生什么。Move VM 里有两个字节码指令:MoveLoc 和 CopyLoc,反映到 Move 语言层面,它们分别对应关键字move和copy。
将变量传递到另一个函数时,MoveLoc 指令被使用,它会被 move。我们可以像下面这样显式使用 move 关键字:
script { use {{sender}}::M; fun main() { let a : Module::T = Module::create(10); M::value(move a); // variable a is moved // local a is dropped } }这段代码是没有问题的,但是我们平常并不需要显示使用 move,缺省 a 会被 move。那么 copy 又是怎么回事呢?
如果想保留变量的值,同时仅将值的副本传递给某函数,则可以使用关键字 copy。
script { use {{sender}}::M; fun main() { let a : Module::T = Module::create(10); // we use keyword copy to clone structure // can be used as `let a_copy = copy a` M::value(copy a); M::value(a); // won't fail, a is still here } }上例中,我们第一次调用函数 value() 时,将变量 a 的副本传递给函数,并保留 a 在本地作用域中,以便第二次调用函数时再次使用它。
使用 copy 后,我们实际上复制了变量值从而增加了程序占用内存的大小。但是如果复制数据数据量比较大,它的内存消耗可能会很高。这里要注意了,在区块链中,交易执行时占用的内存资源是消耗交易费的,每个字节都会影响交易执行费用。因此不加限制的使用 copy 会浪费很多交易费。
现在,是时候学习引用了,它可以帮助我们避免不必要的 copy 从而节省一些费用。
许多编程语言都支持引用。引用是指向变量(通常是内存中的某个片段)的链接,你可以将其传递到程序的其他部分,而无需移动变量值。
引用(标记为&)使我们可以使用值而无需拥有所有权。
我们修改一下上面的示例,看看如何使用引用。
module M { struct T { value: u8 } // ... // ... // instead of passing a value, we'll pass a reference public fun value(t: &T): u8 { t.value } }我们在参数类型 T 前添加了&符号,这样就可以将参数类型T转换成了 T 的引用&T。
Move 支持两种类型的引用:不可变引用 &(例如&T)和可变引用 &mut(例如&mut T)。
不可变的引用允许我们在不更改值的情况下读取值。可变引用赋予我们读取和更改值的能力。
address 0x1 { module M { struct T { value: u8 } // returned value is of non-reference type public fun create(value: u8): T { T { value } } // immutable references allow reading public fun value(t: &T): u8 { t.value } // mutable references allow reading and changing the value public fun change(t: &mut T, value: u8) { t.value = value; } } } script { use 0x1::M; fun main() { let t = M::create(10); // create a reference directly M::change(&mut t, 20); // or write reference to a variable let mut_ref_t = &mut t; M::change(mut_ref_t, 100); // same with immutable ref let value = M::value(&t); // this method also takes only references // printed value will be 100 0x1::Debug::print<u8>(&value); } }使用不可变引用(&)从结构体读取数据,使用可变引用(&mut)修改它们。通过使用适当类型的引用,我们可以更加安全的读取模块,因为它能告诉代码的阅读者,该变量是否会被修改。
Move 通过"Borrow 检查"来控制程序中"引用"的使用,这样有助于防止意外出错。为了理解这一点,我们看一个例子。
address 0x1{ module Borrow { struct B { value: u64 } struct A { b: B } // create A with inner B public fun create(value: u64): A { A { b: B { value } } } // give a mutable reference to inner B public fun ref_from_mut_a(a: &mut A): &mut B { &mut a.b } // change B public fun change_b(b: &mut B, value: u64) { b.value = value; } } } script { use 0x1::Borrow; fun main() { // create a struct A { b: B { value: u64 } } let a = Borrow::create(0); // get mutable reference to B from mut A let mut_a = &mut a; let mut_b = Borrow::ref_from_mut_a(mut_a); // change B Borrow::change_b(mut_b, 100000); // get another mutable reference from A let _ = Borrow::ref_from_mut_a(mut_a); } }上面代码可以成功编译运行,不会报错。这里究竟发生了什么呢?首先,我们使用 A 的可变引用(&mut A)来获取对其内部 struct B 的可变引用(&mut B)。然后我们改变 B。然后可以再次通过 &mut A 获取对 B 的可变引用。
但是,如果我们交换最后两个表达式,即首先尝试创建新的 &mut A,而 &mut B 仍然存在,会出现什么情况呢?
let mut_a = &mut a; let mut_b = Borrow::ref_from_mut_a(mut_a); let _ = Borrow::ref_from_mut_a(mut_a); Borrow::change_b(mut_b, 100000);编译器将会报错:
let _ = Borrow::ref_from_mut_a(mut_a); │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Invalid usage of reference as function argument. Cannot transfer a mutable reference that is being borrowed · 8 │ let mut_b = Borrow::ref_from_mut_a(mut_a); │ ----------------------------- It is still being mutably borrowed by this reference该代码不会编译成功。为什么?因为 &mut A 已经被 &mut B 借用。如果我们再将其作为参数传递,那么我们将陷入一种奇怪的情况,A 可以被更改,但 A 同时又被引用。而 mut_b 应该指向何处呢?
我们得出一些结论:
编译器通过所谓的"借用检查"(最初是Rust语言的概念)来防止上面这些错误。编译器通过建立"借用图",不允许被借用的值被"move"。这就是 Move 在区块链中如此安全的原因之一。
可以从引用创建新的引用,老的引用将被新引用"借用"。可变引用可以创建可变或者不可变引用,而不可变引用只能创建不可变引用。
当一个值被引用时,就无法"move"它了,因为其它值对它有依赖。
可以通过取值运算*来获取引用所指向的值。 取值运算实际上是产生了一个副本,要确保这个值具有 Copy ability。 取值运算不会将原始值 move 到当前作用域,实际上只是生成了一个副本。
module M { struct T has copy {} // value t here is of reference type public fun deref(t: &T): T { *t } }有一个技巧用来复制一个结构体的字段:就是使用*&,引用并取值。我们来看一个例子:
module M { struct H has copy {} struct T { inner: H } // ... // we can do it even from immutable reference! public fun copy_inner(t: &T): H { *&t.inner } }通过使用*&(编译器会建议这样做),我们复制了结构体的内部值。
基本类型非常简单,它们不需要作为引用传递,缺省会被复制。当基本类型的值被传给函数时,相当于使用了copy关键字,传递进函数的是它们的副本。当然你可以使用move关键字强制不产生副本,但是由于基本类型的大小很小,复制它们其实开销很小,甚至比通过引用或者"move"传递它们开销更小。
script { use {{sender}}::M; fun main() { let a = 10; M::do_smth(a); let _ = a; } }也就是说,即使我们没有将a作为引用传递,该脚本也会编译。我们也无需添加copy,因为 VM 已经帮组我们添加了。
泛型是具体类型或其他属性的抽象替代品。实际上,泛型允许我们只编写单个函数,而该函数可以应用于任何类型。这种函数也被称为模板 —— 一个可以应用于任何类型的模板处理程序。
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实际并不存在,它只是任何类型的占位符。 函数中的泛型 现在让我们为上面的结构体创建一个构造函数,该构造函数将首先使用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。最后一个看起来有些复杂,但是一旦你习惯了,并且理解了泛型是如何工作的,它成为你日常工作的好帮手。 继续下一步之前,让我们做一个简单的回顾。我们通过将泛型添加到Box结构体中,使Box变得抽象了。与 Box 能提供的功能相比,它的定义相当简单。现在,我们可以使用任何类型创建Box,u64 或 address,甚至另一个 Box 或另一个结构体。 abilities 限制符 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 请记住 + 这个语法符号,第一眼看上去可能不太适应,因为很少有语言在关键字列表中使用 +。 另一个需要被提及的是结构体的成员必须和结构体具有相同的 abilities (除了key以外)。这个很容易理解,如果结构体具有 copy ability,那么它的成员也必须能被 copy,否则结构体作为一个整体不能被 copy。Move 编译器允许代码不遵守这样的逻辑,但是运行时会出问题。 address 0x1{ 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() { 0x1::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 } } } 未使用的类型参数? 并非泛型中指定的每种类型参数都必须被使用。看这个例子: 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概念后,就会知道为什么这种定义很重要。目前,就当这只是使用泛型的一种方法。 向量vector 我们已经非常熟悉结构体类型了,它使我们能够创建自己的类型并存储复杂数据。但是有时我们需要动态、可扩展和可管理的功能。为此,Move 提供了向量 Vector。 Vector 是用于存储数据集合的内置类型。集合的数据可以是任何类型(但仅一种)。Vector 功能实际上是由 VM 提供的,不是由 Move 语言提供的,使用它的唯一方法是使用标准库和 native 函数。 script { use 0x1::Vector; fun main() { // use generics to create an emtpy vector let a = Vector::empty<u8>(); let i = 0; // let's fill it with data while (i < 10) { Vector::push_back(&mut a, i); i = i + 1; }; // now print vector length let a_len = Vector::length(&a); 0x1::Debug::print<u64>(&a_len); // then remove 2 elements from it Vector::pop_back(&mut a); Vector::pop_back(&mut a); // and print length again let a_len = Vector::length(&a); 0x1::Debug::print<u64>(&a_len); } } Vector 最多可以存储 18446744073709551615u64(u64最大值)个非引用类型的值。要了解它如何帮助我们管理大型数据,我们试着编写一个模块。 module Shelf { use 0x1::Vector; struct Box<T> { value: T } struct Shelf<T> { boxes: vector<Box<T>> } public fun create_box<T>(value: T): Box<T> { Box { value } } // this method will be inaccessible for non-copyable contents public fun value<T: copy>(box: &Box<T>): T { *&box.value } public fun create<T>(): Shelf<T> { Shelf { boxes: Vector::empty<Box<T>>() } } // box value is moved to the vector public fun put<T>(shelf: &mut Shelf<T>, box: Box<T>) { Vector::push_back<Box<T>>(&mut shelf.boxes, box); } public fun remove<T>(shelf: &mut Shelf<T>): Box<T> { Vector::pop_back<Box<T>>(&mut shelf.boxes) } public fun size<T>(shelf: &Shelf<T>): u64 { Vector::length<Box<T>>(&shelf.boxes) } } 我们将创建一个 Shelf,为其提供几个 Box,并观察如何在模块中使用 vector: script { use {{sender}}::Shelf; fun main() { // create shelf and 2 boxes of type u64 let shelf = Shelf::create<u64>(); let box_1 = Shelf::create_box<u64>(99); let box_2 = Shelf::create_box<u64>(999); // put both boxes to shelf Shelf::put(&mut shelf, box_1); Shelf::put(&mut shelf, box_2); // prints size - 2 0x1::Debug::print<u64>(&Shelf::size<u64>(&shelf)); // then take one from shelf (last one pushed) let take_back = Shelf::remove(&mut shelf); let value = Shelf::value<u64>(&take_back); // verify that the box we took back is one with 999 assert(value == 999, 1); // and print size again - 1 0x1::Debug::print<u64>(&Shelf::size<u64>(&shelf)); } } 向量非常强大,它使我们可以存储大量数据,并可以在索引的存储中使用它。 内联 Vector 定义的十六进制数组和字符串 Vector 也可以表示字符串。VM支持将vector作为参数传递给main脚本中的函数。 也可以使用十六进制字面值(literal)在脚本或模块中定义vector: script { use 0x1::Vector; // this is the way to accept arguments in main fun main(name: vector<u8>) { let _ = name; // and this is how you use literals // this is a "hello world" string! let str = x"68656c6c6f20776f726c64"; // hex literal gives you vector<u8> as well Vector::length<u8>(&str); } } 更简单的方法是使用字符串字面值(literal): script { fun main() { let _ = b"hello world"; } } 它们被视为 ASCII 字符串,也被解释为 vector。 可编程 Resource Move 的主要功能是提供了自定义 Resource 类型。Resource 类型为安全的数字资产编码具提供了丰富的可编程性。 Resource 在Move语言中就是普通的值。它们可以作为数据结构被存储,作为参数被传递给函数,也可以从函数中返回。 Resource 是一种特殊的结构体,可以在 Move 代码中定义和创建,也可以使用现有的 Resource。因此,我们可以像使用任何其它数据(比如向量或结构体)那样来管理数字资产。 Move 类型系统为 Resource 提供了特殊的安全保证。Resource 永远不能被复制,重用或丢弃。Resource 类型只能由定义该类型的模块创建或销毁。这些检查由 Move 虚拟机通过字节码校验强制执行。Move 虚拟机将拒绝运行任何尚未通过字节码校验的代码。 Signer Signer是一种原生的类似 Resource 的不可复制的类型,它包含了交易发送者的地址。 Signer类型代表了发送者权限。换句话说,使用signer意味着可以访问发送者的地址和 Resource。它与signature没有直接关系,就Move VM而言,它仅表示发送者。 Signer只有一种 ability: Drop。
