Move 参考手册

欢迎使用 Move,这是一种用于安全资产编程的下一代语言。它的主要用例是在区块链环境中,Move 程序用于构建状态变化。Move 允许开发人员编写程序,以灵活管理和转移资产,同时提供对这些资产攻击的安全性和保护。然而,Move 也考虑到了区块链环境之外的用例。

Move 借鉴了 Rust 的经验,通过使用具有 move(因此得名)语义的资源类型作为数字资产(例如货币)的明确表示。

Modules(模块)

模块 是定义类型及操作这些类型的函数的核心程序单元。结构类型定义了 Move 存储的模式,而模块函数定义了与这些类型的值交互的规则。虽然模块本身也存储在存储中,但在 Move 程序内部是不可访问的。在区块链环境中,模块存储在链上,通常称为 "发布" 过程。在发布后,可以根据特定 Move 实例的规则调用 entrypublic 函数。

语法

模块具有以下语法:

module <address>::<identifier> {
    (<use> | <type> | <function> | <constant>)*
}

其中 <address> 是指定模块所属包的有效 地址

例如:

module 0x42::test {
    public struct Example has copy, drop { i: u64 }

    use std::debug;

    const ONE: u64 = 1;

    public fun print(x: u64) {
        let sum = x + ONE;
        let example = Example { i: sum };
        debug::print(&sum)
    }
}

名称

module test_addr::test 部分指定了模块 test 将在名称为 test_addr 的包设置中分配的数字 地址 值下进行发布。

通常应使用 命名地址 来声明模块(而不是直接使用数字值)。例如:

module test_addr::test {
    public struct Example has copy, drop { a: address }

    friend test_addr::another_test;

    public fun print() {
        let example = Example { a: @test_addr };
        debug::print(&example)
    }
}

这些命名地址通常与 的名称匹配。

因为命名地址仅存在于源语言级别和编译过程中,在字节码级别上,命名地址将完全替换为其值。例如,如果我们有以下代码:

fun example() {
    my_addr::m::foo(@my_addr);
}

并且将其编译时设置 my_addr 设置为 0xC0FFEE,则其操作上等效于:

fun example() {
    0xC0FFEE::m::foo(@0xC0FFEE);
}

尽管在源级别上这两种访问方式是等效的,但最佳实践是始终使用命名地址而不是分配给该地址的数字值。

模块名可以以小写字母 az 或大写字母 AZ 开始。在第一个字符之后,模块名可以包含下划线 _、字母 az、字母 AZ 或数字 09

module a::my_module {}
module a::foo_bar_42 {}

通常,模块名以小写字母开头。名为 my_module 的模块应存储在名为 my_module.move 的源文件中。

成员

模块块内的所有成员可以以任何顺序出现。基本上,模块是 typesfunctions 的集合。use 关键字用于引用其他模块的成员。const 关键字定义可以在模块函数中使用的常量。

friend 语法是一种已废弃的概念,用于指定一组受信任的模块列表。该概念已被 public(package) 取代。

原始类型

原始类型是语言的基本构建块。

这些原始类型可以单独使用,也可以用于构建更复杂的用户定义类型,例如在 struct 中。

这些原始类型可以与其他类型一起使用。

点击链接查看详细信息。

整数类型

Move 语言支持六种无符号整数类型:u8u16u32u64u128u256。这些类型的值范围从 0 到根据类型大小决定的最大值。

类型值范围
无符号 8 位整数,u80 到 28 - 1
无符号 16 位整数,u160 到 216 - 1
无符号 32 位整数,u320 到 232 - 1
无符号 64 位整数,u640 到 264 - 1
无符号 128 位整数,u1280 到 2128 - 1
无符号 256 位整数,u2560 到 2256 - 1

字面值

这些类型的字面值可以用数字序列表示(例如 112)或十六进制表示(例如 0xFF)。可以选择性地在字面值后加上类型后缀(例如 112u8)。如果未指定类型,编译器会尝试从字面值所在的上下文推断类型。如果无法推断,则默认假设为 u64

数字字面值可以用下划线分隔以增强可读性(例如 1_234_56781_000u1280xAB_CD_12_35)。

如果字面值超出了指定(或推断)类型的范围,会报告错误。

示例

// 带显式注释的字面值
let explicit_u8 = 1u8;
let explicit_u16 = 1u16;
let explicit_u32 = 1u32;
let explicit_u64 = 2u64;
let explicit_u128 = 3u128;
let explicit_u256 = 1u256;
let explicit_u64_underscored = 154_322_973u64;

// 简单推断的字面值
let simple_u8: u8 = 1;
let simple_u16: u16 = 1;
let simple_u32: u32 = 1;
let simple_u64: u64 = 2;
let simple_u128: u128 = 3;
let simple_u256: u256 = 1;

// 复杂推断的字面值
let complex_u8 = 1; // 推断为 u8
// 右侧的移位参数必须是 u8
let _unused = 10 << complex_u8;

let x: u8 = 38;
let complex_u8 = 2; // 推断为 u8
// `+` 的参数必须是相同类型
let _unused = x + complex_u8;

let complex_u128 = 133_876; // 推断为 u128
// 从函数参数类型推断
function_that_takes_u128(complex_u128);

// 字面值可以用十六进制表示
let hex_u8: u8 = 0x1;
let hex_u16: u16 = 0x1BAE;
let hex_u32: u32 = 0xDEAD80;
let hex_u64: u64 = 0xCAFE;
let hex_u128: u128 = 0xDEADBEEF;
let hex_u256: u256 = 0x1123_456A_BCDE_F;

操作

算术运算

每种整数类型都支持相同的检查算术运算。对于所有这些运算,两个操作数(左侧和右侧操作数)必须 是相同类型。如果需要对不同类型的值进行操作,必须先进行类型转换。同样,如果预期运算结果会超出整数类型的范围,需在运算前进行类型转换到更大类型。

所有算术运算在发生溢出、下溢或除以零等数学错误时会中止。

语法操作中止条件
+加法结果超出整数类型的最大范围
-减法结果小于零
*乘法结果超出整数类型的最大范围
%模除法除数为 0
/截断除法除数为 0

位运算

整数类型支持以下位运算,这些运算将每个数字视为一系列的 0 或 1 位,而不是数值。

位运算不会中止。

语法操作描述
&位与对每个位进行逐位的布尔与运算
|位或对每个位进行逐位的布尔或运算
^位异或对每个位进行逐位的布尔异或运算

位移运算

与位运算类似,每种整数类型支持位移运算。但与其他运算不同,右侧操作数(要移位的位数)必须_始终_为 u8 类型,不必与左侧操作数(要移位的数字)类型匹配。

如果移位位数大于或等于 8163264128256(对于 u8u16u32u64u128u256),位移运算会中止。

语法操作中止条件
<<左移移位位数大于整数类型的位数
>>右移移位位数大于整数类型的位数

比较运算

整数类型是 Move 语言中唯一可以使用比较运算符的类型。两个操作数需要是相同类型。如果需要比较不同类型的整数,必须先进行类型转换

比较运算不会中止。

语法操作
<小于
>大于
<=小于等于
>=大于等于

相等运算

与所有具有 drop 能力的类型一样,所有整数类型都支持 "相等""不相等" 运算。两个操作数需要是相同类型。如果需要比较不同类型的整数,必须先进行类型转换

相等运算不会中止。

语法操作
==相等
!=不相等

更多细节请参见相等部分。

类型转换

一种整数类型可以转换为另一种整数类型。整数是 Move 语言中唯一支持类型转换的类型。

类型转换_不会_截断。如果结果超出指定类型的范围,转换会中止。

语法操作中止条件
(e as T)将整数表达式 e 转换为整数类型 Te 超出 T 的表示范围

在这里,e 的类型必须是 8163264128256T 必须是 u8u16u32u64u128u256

例如:

  • (x as u8)
  • (y as u16)
  • (873u16 as u32)
  • (2u8 as u64)
  • (1 + 3 as u128)
  • (4/2 + 12345 as u256)

所有权

与语言中其他标量值一样,整数值是隐式可复制的,这意味着它们可以在不需要显式指令(如 copy)的情况下进行复制。

布尔类型

bool 是 Sui Move 语言中用于表示布尔值 truefalse 的原始类型。

字面值

布尔类型的字面值可以是 truefalse

操作

逻辑运算

bool 支持三种逻辑运算:

语法描述等效表达式
&&短路逻辑与p && q 等效于 if (p) q else false
||短路逻辑或p || q 等效于 if (p) true else q
!逻辑非!p 等效于 if (p) false else true

控制流

bool 值在 Sui Move 的多个控制流结构中使用:

所有权

与语言中其他标量值一样,布尔值是隐式可复制的,这意味着它们可以在不需要显式指令(如 copy)的情况下进行复制。

地址

address是Move语言中的一个内置类型,用于表示存储中的位置(有时也称为账户)。一个address值是一个256位(32字节)的标识符。Move使用地址来区分模块的包,每个包都有自己的地址和模块。特定的Move部署也可能使用address值进行存储操作。

对于Sui来说,address用于表示"账户",也通过强类型包装器(使用sui::object::UIDsui::object::ID)表示对象。

虽然address在底层是一个256位整数,但Move地址是有意不透明的——它们不能从整数创建,不支持算术运算,也不能被修改。特定的Move部署可能有native函数来启用其中一些操作(例如,从字节vector<u8>创建一个address),但这些不是Move语言本身的一部分。

虽然存在运行时地址值(address类型的值),但它们在运行时_不能_用于访问模块。

地址及其语法

地址有两种形式,命名的或数字的。命名地址的语法遵循Move中任何命名标识符的相同规则。数字地址的语法不限于十六进制编码的值,任何有效的u256数值都可以用作地址值,例如,420xCAFE10_000都是有效的数字地址字面量。

为了区分地址是否在表达式上下文中使用,使用地址的语法根据使用的上下文而有所不同:

  • 当地址作为表达式使用时,地址必须以@字符为前缀,即@<数值>@<命名地址标识符>
  • 在表达式上下文之外,地址可以不带前导@字符书写,即<数值><命名地址标识符>

一般来说,你可以把@看作是一个操作符,它将地址从命名空间项转换为表达式项。

命名地址

命名地址是一个特性,允许在使用地址的任何地方使用标识符来代替数值,而不仅仅是在值级别。命名地址在Move包中被声明和绑定为顶层元素(在模块和脚本之外),或作为参数传递给Move编译器。

命名地址只存在于源语言级别,在字节码级别将完全被其值替代。因此,应该通过模块的命名地址而不是编译期间分配给命名地址的数值来访问模块和模块成员。所以虽然use my_addr::foo等同于use 0x2::foo(如果my_addr被分配为0x2),但最佳实践是始终使用my_addr名称。

示例

// 0x0000000000000000000000000000000000000000000000000000000000000001的简写
let a1: address = @0x1;
// 0x0000000000000000000000000000000000000000000000000000000000000042的简写
let a2: address = @0x42;
// 0x00000000000000000000000000000000000000000000000000000000DEADBEEF的简写
let a3: address = @0xDEADBEEF;
// 0x000000000000000000000000000000000000000000000000000000000000000A的简写
let a4: address = @0x0000000000000000000000000000000A;
// 将命名地址`std`的值赋给`a5`
let a5: address = @std;
// 任何有效的数值都可以用作地址
let a6: address = @66;
let a7: address = @42_000;

module 66::some_module {   // 不在表达式上下文中,所以不需要@
    use 0x1::other_module; // 不在表达式上下文中,所以不需要@
    use std::vector;       // 可以使用命名地址作为命名空间项
    ...
}

module std::other_module {  // 声明模块时可以使用命名地址
    ...
}

这是关于Move语言中向量(vector)类型的详细介绍。我会将其翻译成简单易懂的中文,同时保留关键的技术术语:

向量

vector<T>是Move提供的唯一原始集合类型。vector<T>T类型元素的同类集合,可以通过在"末端"推入/弹出值来增长或缩小。

vector<T>可以用任何类型T实例化。例如,vector<u64>,vector<address>,vector<0x42::my_module::MyData>,和vector<vector<u8>>都是有效的向量类型。

字面量

通用vector字面量

任何类型的向量都可以用vector字面量创建。

语法类型描述
vector[]vector[]: vector<T>,其中T是任何单一的非引用类型空向量
vector[e1, ..., en]vector[e1, ..., en]: vector<T>,其中e_i: T0 < i <= nn > 0n个元素的向量(长度为n)

在这些情况下,vector的类型是从元素类型或向量的使用中推断出来的。如果无法推断类型,或者为了更清晰,可以显式指定类型:

vector<T>[]: vector<T>
vector<T>[e1, ..., en]: vector<T>

向量字面量示例

(vector[]: vector<bool>);
(vector[0u8, 1u8, 2u8]: vector<u8>);
(vector<u128>[]: vector<u128>);
(vector<address>[@0x42, @0x100]: vector<address>);

vector<u8>字面量

Move中向量的一个常见用例是表示"字节数组",用vector<u8>表示。这些值通常用于加密目的,如公钥或哈希结果。这些值非常常见,以至于提供了特定的语法使值更易读,而不是必须使用vector[]来指定每个单独的u8值。

目前支持两种类型的vector<u8>字面量,字节字符串_和_十六进制字符串

字节字符串

字节字符串是带有b前缀的带引号字符串字面量,例如b"Hello!\n"

这些是ASCII编码的字符串,允许使用转义序列。目前,支持的转义序列有:

转义序列描述
\n换行(或换行符)
\r回车
\t制表符
\\反斜杠
\0空字符
\"引号
\xHH十六进制转义,插入十六进制字节序列HH

十六进制字符串

十六进制字符串是带有x前缀的带引号字符串字面量,例如x"48656C6C6F210A"

每个字节对,范围从00FF,被解释为十六进制编码的u8值。所以每个字节对对应结果vector<u8>中的一个条目。

字符串字面量示例

fun byte_and_hex_strings() {
    assert!(b"" == x"", 0);
    assert!(b"Hello!\n" == x"48656C6C6F210A", 1);
    assert!(b"\x48\x65\x6C\x6C\x6F\x21\x0A" == x"48656C6C6F210A", 2);
    assert!(
        b"\"Hello\tworld!\"\n \r \\Null=\0" ==
            x"2248656C6C6F09776F726C6421220A200D205C4E756C6C3D00",
        3
    );
}

操作

vector通过Move标准库中的std::vector模块支持以下操作:

函数描述是否中止?
vector::empty<T>(): vector<T>创建一个可以存储T类型值的空向量从不
vector::singleton<T>(t: T): vector<T>创建一个包含t的大小为1的向量从不
vector::push_back<T>(v: &mut vector<T>, t: T)t添加到v的末尾从不
vector::pop_back<T>(v: &mut vector<T>): T移除并返回v中的最后一个元素如果v为空
vector::borrow<T>(v: &vector<T>, i: u64): &T返回索引i处的T的不可变引用如果i不在范围内
vector::borrow_mut<T>(v: &mut vector<T>, i: u64): &mut T返回索引i处的T的可变引用如果i不在范围内
vector::destroy_empty<T>(v: vector<T>)删除v如果v不为空
vector::append<T>(v1: &mut vector<T>, v2: vector<T>)v2中的元素添加到v1的末尾从不
vector::contains<T>(v: &vector<T>, e: &T): bool如果e在向量v中返回true。否则,返回false从不
vector::swap<T>(v: &mut vector<T>, i: u64, j: u64)交换向量v中第i和第j个索引处的元素如果ij超出范围
vector::reverse<T>(v: &mut vector<T>)原地反转向量v中元素的顺序从不
vector::index_of<T>(v: &vector<T>, e: &T): (bool, u64)如果e在索引i处的向量v中,返回(true, i)。否则,返回(false, 0)从不
vector::remove<T>(v: &mut vector<T>, i: u64): T移除向量v的第i个元素,移动所有后续元素。这是O(n)操作,并保持向量中元素的顺序如果i超出范围
vector::swap_remove<T>(v: &mut vector<T>, i: u64): T将向量v的第i个元素与最后一个元素交换,然后弹出该元素。这是O(1)操作,但不保持向量中元素的顺序如果i超出范围

随着时间的推移,可能会添加更多操作。

示例

use std::vector;

let mut v = vector::empty<u64>();
vector::push_back(&mut v, 5);
vector::push_back(&mut v, 6);

assert!(*vector::borrow(&v, 0) == 5, 42);
assert!(*vector::borrow(&v, 1) == 6, 42);
assert!(vector::pop_back(&mut v) == 6, 42);
assert!(vector::pop_back(&mut v) == 5, 42);

销毁和复制vector

vector<T>的某些行为取决于元素类型T的能力。例如,包含没有drop能力的元素的向量不能像上面示例中的v那样被隐式丢弃--它们必须使用vector::destroy_empty显式销毁。

注意,除非vec包含零个元素,否则vector::destroy_empty将在运行时中止:

fun destroy_any_vector<T>(vec: vector<T>) {
    vector::destroy_empty(vec) // 删除这行将导致编译器错误
}

但对于丢弃包含具有drop能力的元素的向量不会发生错误:

fun destroy_droppable_vector<T: drop>(vec: vector<T>) {
    // 有效!
    // 不需要显式做任何事来销毁向量
}

同样,除非元素类型具有copy能力,否则向量不能被复制。换句话说,当且仅当T具有copy能力时,vector<T>才具有copy能力。请注意,如果需要,它将被隐式复制:

let x = vector[10];
let y = x; // 隐式复制
let z = x;
(y, z)

请记住,复制大向量可能很昂贵。如果这是一个问题,注释intended用法可以防止意外复制。例如,

let x = vector[10];
let y = move x;
let z = x; // 错误! x已被移动
(y, z)

有关更多详细信息,请参阅类型能力泛型部分。

所有权

上文所述,只有当元素可以被复制时,vector值才能被复制。在这种情况下,可以通过copy解引用*进行复制。

这是一个关于Move语言中引用的详细介绍。我会将其翻译成简单易懂的中文,同时保留关键的技术术语:

引用

Move有两种类型的引用:不可变引用&和可变引用&mut。不可变引用是只读的,不能修改底层值(或其任何字段)。可变引用允许通过该引用进行修改。Move的类型系统强制执行所有权规则,以防止引用错误。

引用操作符

Move提供了创建和扩展引用以及将可变引用转换为不可变引用的操作符。这里我们用e: T表示"表达式e的类型为T"。

语法类型描述
&e&T,其中e: TT不是引用类型创建e的不可变引用
&mut e&mut T,其中e: TT不是引用类型创建e的可变引用
&e.f&T,其中e.f: T创建结构体e的字段f的不可变引用
&mut e.f&mut T,其中e.f: T创建结构体e的字段f的可变引用
freeze(e)&T,其中e: &mut T将可变引用e转换为不可变引用

&e.f&mut e.f操作符既可以用于创建新的引用到结构体中,也可以用于扩展现有引用:

let s = S { f: 10 };
let f_ref1: &u64 = &s.f; // 可以
let s_ref: &S = &s;
let f_ref2: &u64 = &s_ref.f // 也可以

多字段的引用表达式只要两个结构体在同一模块中就可以工作:

public struct A { b: B }
public struct B { c : u64 }
fun f(a: &A): &u64 {
    &a.b.c
}

最后,请注意不允许引用的引用:

let x = 7;
let y: &u64 = &x;
let z: &&u64 = &y; // 错误! 无法编译

通过引用读写

可变和不可变引用都可以被读取以产生被引用值的副本。

只有可变引用可以被写入。写入*x = v会丢弃之前存储在x中的值,并用v更新它。

这两种操作都使用类似C的*语法。但请注意,读取是一个表达式,而写入是必须发生在等号左侧的变更。

语法类型描述
*eT,其中e&T&mut T读取e指向的值
*e1 = e2(),其中e1: &mut Te2: Te2更新e1中的值

为了能读取引用,底层类型必须具有copy能力,因为读取引用会创建值的新副本。这条规则防止了资产被复制:

fun copy_coin_via_ref_bad(c: Coin) {
    let c_ref = &c;
    let counterfeit: Coin = *c_ref; // 不允许!
    pay(c);
    pay(counterfeit);
}

相对地:为了能写入引用,底层类型必须具有drop能力,因为写入引用会丢弃(或"删除")旧值。这条规则防止了资源值被销毁:

fun destroy_coin_via_ref_bad(mut ten_coins: Coin, c: Coin) {
    let ref = &mut ten_coins;
    *ref = c; // 错误! 不允许--会销毁10个硬币!
}

freeze推断

可变引用可以在期望不可变引用的上下文中使用:

let mut x = 7;
let y: &u64 = &mut x;

这是因为在底层,编译器会在需要的地方插入freeze指令。这里有更多freeze推断的示例:

fun takes_immut_returns_immut(x: &u64): &u64 { x }

// 在返回值上进行freeze推断
fun takes_mut_returns_immut(x: &mut u64): &u64 { x }

fun expression_examples() {
    let mut x = 0;
    let mut y = 0;
    takes_immut_returns_immut(&x); // 无推断
    takes_immut_returns_immut(&mut x); // 推断为freeze(&mut x)
    takes_mut_returns_immut(&mut x); // 无推断

    assert!(&x == &mut y, 42); // 推断为freeze(&mut y)
}

fun assignment_examples() {
    let x = 0;
    let y = 0;
    let imm_ref: &u64 = &x;

    imm_ref = &x; // 无推断
    imm_ref = &mut y; // 推断为freeze(&mut y)
}

子类型

通过这种freeze推断,Move类型检查器可以将&mut T视为&T的子类型。如上所示,这意味着在任何使用&T值的表达式中,也可以使用&mut T值。这个术语在错误消息中用于简洁地表示在需要&T的地方提供了&mut T。例如:

module a::example {
    fun read_and_assign(store: &mut u64, new_value: &u64) {
        *store = *new_value
    }

    fun subtype_examples() {
        let mut x: &u64 = &0;
        let mut y: &mut u64 = &mut 1;

        x = &mut 1; // 有效
        y = &2; // 错误! 无效!

        read_and_assign(y, x); // 有效
        read_and_assign(x, y); // 错误! 无效!
    }
}

将产生以下错误消息:

错误:

    ┌── example.move:11:9 ───
    │
 12 │         y = &2; // 无效!
    │         ^ 对局部变量'y'的无效赋值
    ·
 12 │         y = &2; // 无效!
    │             -- 类型: '&{integer}'
    ·
  9 │         let mut y: &mut u64 = &mut 1;
    │                    -------- 不是子类型: '&mut u64'
    │

错误:

    ┌── example.move:14:9 ───
    │
 15 │         read_and_assign(x, y); // 无效!
    │         ^^^^^^^^^^^^^^^^^^^^^ 对'a::example::read_and_assign'的无效调用。参数'store'无效
    ·
  8 │         let mut x: &u64 = &0;
    │                    ---- 类型: '&u64'
    ·
  3 │     fun read_and_assign(store: &mut u64, new_value: &u64) {
    │                                -------- 不是子类型: '&mut u64'
    │

目前唯一具有子类型的其他类型是元组

所有权

可变和不可变引用都可以随时被复制和扩展,即使存在同一引用的现有副本或扩展:

fun reference_copies(s: &mut S) {
  let s_copy1 = s; // 可以
  let s_extension = &mut s.f; // 也可以
  let s_copy2 = s; // 仍然可以
  ...
}

这可能会让熟悉Rust所有权系统的程序员感到惊讶,Rust会拒绝上面的代码。Move的类型系统在处理复制时更加宽松,但在写入前确保可变引用的唯一所有权方面同样严格。

引用不能被存储

引用和元组是_唯一_不能作为结构体字段值存储的类型,这也意味着它们不能存在于存储或对象中。在程序执行期间创建的所有引用都会在Move程序终止时被销毁;它们完全是短暂的。这也适用于所有没有store能力的类型:任何非store类型的值必须在程序终止前被销毁。能力,但请注意引用和元组更进一步,从一开始就不允许存在于结构体中。

这是Move和Rust的另一个区别,Rust允许在结构体中存储引用。

我们可以想象一个更复杂、更具表现力的类型系统,允许在结构体中存储引用。我们可以允许在没有store能力的结构体中使用引用,但核心困难在于Move有一个相当复杂的系统来跟踪静态引用安全性。类型系统的这个方面也需要扩展以支持在结构体中存储引用。简而言之,Move的引用安全系统需要扩展以支持存储引用,这是我们在语言演化过程中一直在关注的问题。

元组与单位类型

Move 并未完全支持元组,这与其他将元组作为一等公民的语言有所不同。然而,为了支持多返回值,Move 提供了类似元组的表达式。这些表达式在运行时不会生成具体的值(字节码中不存在元组),因此它们有很大的局限性:

  • 只能出现在表达式中(通常在函数的返回位置)。
  • 不能绑定到局部变量。
  • 不能存储在结构体中。
  • 元组类型不能用于实例化泛型。

类似地,unit () 类型是 Move 源语言为了基于表达式的设计而创建的。单位值 () 在运行时不会产生任何值。可以将单位 () 视为一个空元组,适用于所有对元组的限制。

考虑到这些限制,在语言中使用元组可能会感到奇怪。但在其他语言中,元组最常见的用例之一是允许函数返回多个值。一些语言通过强迫用户编写包含多个返回值的结构体来解决这个问题。然而,在 Move 中,你不能在结构体中放置引用。这要求 Move 支持多返回值。在字节码层面,这些多返回值全部压入堆栈。在源代码层面,这些多返回值使用元组表示。

字面量

元组通过在括号内使用逗号分隔的表达式列表创建。

语法类型描述
()(): ()单位类型,空元组,或 0 元素的元组
(e1, ..., en)(e1, ..., en): (T1, ..., Tn) 其中 e_i: Ti 满足 0 < i <= nn > 0n 元组,n 元素的元组,包含 n 个元素

注意 (e) 并没有类型 (e): (t),换句话说,不存在单元素元组。如果括号内只有一个元素,则括号仅用于消除歧义,没有其他特殊含义。

有时,包含两个元素的元组称为"对",包含三个元素的元组称为"三元组"。

示例

module 0x42::example {
    // 以下三个函数是等价的

    // 当没有提供返回类型时,假定为 `()`
    fun returns_unit_1() { }

    // 空表达式块中有一个隐式的 () 值
    fun returns_unit_2(): () { }

    // 显式版本的 `returns_unit_1` 和 `returns_unit_2`
    fun returns_unit_3(): () { () }

    fun returns_3_values(): (u64, bool, address) {
        (0, false, @0x42)
    }
    fun returns_4_values(x: &u64): (&u64, u8, u128, vector<u8>) {
        (x, 0, 1, b"foobar")
    }
}

操作

目前,对元组唯一可以执行的操作是解构。

解构

对于任何大小的元组,都可以在 let 绑定或赋值中解构。

例如:

module 0x42::example {
    // 以下三个函数是等价的
    fun returns_unit() {}
    fun returns_2_values(): (bool, bool) { (true, false) }
    fun returns_4_values(x: &u64): (&u64, u8, u128, vector<u8>) { (x, 0, 1, b"foobar") }

    fun examples(cond: bool) {
        let () = ();
        let (mut x, mut y): (u8, u64) = (0, 1);
        let (mut a, mut b, mut c, mut d) = (@0x0, 0, false, b"");

        () = ();
        (x, y) = if (cond) (1, 2) else (3, 4);
        (a, b, c, d) = (@0x1, 1, true, b"1");
    }

    fun examples_with_function_calls() {
        let () = returns_unit();
        let (mut x, mut y): (bool, bool) = returns_2_values();
        let (mut a, mut b, mut c, mut d) = returns_4_values(&0);

        () = returns_unit();
        (x, y) = returns_2_values();
        (a, b, c, d) = returns_4_values(&1);
    }
}

更多详情请参见 Move 变量

子类型

与引用一样,元组是 Move 中唯一具有子类型的类型。元组的子类型仅在引用中的协变方式存在。

例如:

let x: &u64 = &0;
let y: &mut u64 = &mut 1;

// (&u64, &mut u64) 是 (&u64, &u64) 的子类型
// 因为 &mut u64 是 &u64 的子类型
let (a, b): (&u64, &u64) = (x, y);

// (&mut u64, &mut u64) 是 (&u64, &u64) 的子类型
// 因为 &mut u64 是 &u64 的子类型
let (c, d): (&u64, &u64) = (y, y);

// 错误! (&u64, &mut u64) 不是 (&mut u64, &mut u64) 的子类型
// 因为 &u64 不是 &mut u64 的子类型
let (e, f): (&mut u64, &mut u64) = (x, y);

所有权

如前所述,元组值在运行时并不真正存在。目前它们不能存储到局部变量中(但未来可能会添加此功能)。因此,元组只能移动,不能复制,因为复制它们需要首先将其放入局部变量中。

Local Variables and Scope(局部变量和作用域)

在 Move 中,局部变量是词法(静态)作用域的。新变量通过关键字 let 引入,这将遮蔽任何具有相同名称的先前局部变量。标记为 mut 的局部变量是可变的,可以直接修改或通过可变引用进行修改。

声明局部变量

let 绑定

Move 程序使用 let 将变量名称绑定到值:

let x = 1;
let y = x + x;

let 也可以在不将值绑定到局部变量的情况下使用。

let x;

然后可以在稍后为局部变量赋值。

let x;
if (cond) {
  x = 1;
} else {
  x = 0;
}

在无法提供默认值时,这在从循环中提取值时非常有用。

let x;
let cond = true;
let i = 0;
loop {
    let (res, cond) = foo(i);
    if (!cond) {
        x = res;
        break;
    };
    i = i + 1;
}

要在赋值后修改局部变量,或者借用它的可变引用 &mut,必须将其声明为 mut

let mut x = 0;
if (cond) x = x + 1;
foo(&mut x);

有关更多详细信息,请参见下面的 赋值 部分。

变量必须在使用前赋值

Move 的类型系统防止局部变量在赋值前使用。

let x;
x + x // 错误!x 在赋值前被使用
let x;
if (cond) x = 0;
x + x // 错误!x 并不是在所有情况下都有值
let x;
while (cond) x = 0;
x + x // 错误!x 并不是在所有情况下都有值

有效的变量名

变量名可以包含下划线 _、字母 az、字母 AZ 和数字 09。变量名必须以下划线 _ 或字母 az 开头。它们不能以大写字母开头。

// 所有合法的变量名
let x = e;
let _x = e;
let _A = e;
let x0 = e;
let xA = e;
let foobar_123 = e;

// 所有非法的变量名
let X = e; // 错误!
let Foo = e; // 错误!

类型注解

局部变量的类型几乎总是可以通过 Move 的类型系统推断出来。然而,Move 允许显式的类型注解,这对于可读性、清晰性或调试非常有用。添加类型注解的语法是:

let x: T = e; // "变量 x 的类型为 T,被初始化为表达式 e"

一些显式类型注解的例子:

module 0x42::example {

    public struct S { f: u64, g: u64 }

    fun annotated() {
        let u: u8 = 0;
        let b: vector<u8> = b"hello";
        let a: address = @0x0;
        let (x, y): (&u64, &mut u64) = (&0, &mut 1);
        let S { f, g: f2 }: S = S { f: 0, g: 1 };
    }
}

注意,类型注解必须始终位于模式的右侧:

// 错误!应为 let (x, y): (&u64, &mut u64) = ...
let (x: &u64, y: &mut u64) = (&0, &mut 1);

何时需要注解

在某些情况下,如果类型系统无法推断类型,则需要局部类型注解。这通常发生在无法推断泛型类型的类型参数时。例如:

let _v1 = vector[]; // 错误!
//        ^^^^^^^^ 无法推断此类型。尝试添加注解
let v2: vector<u64> = vector[]; // 无错误

在较少见的情况下,如果类型系统无法推断出分歧代码(所有后续代码都不可达)的类型,也可能需要类型注解。returnabort 都是表达式,可以具有任何类型。如果一个 loop 具有 break,则其类型为 ()(或者如果具有 break ee: T,则类型为 T),但如果没有跳出 loop,它可以具有任何类型。如果这些类型无法推断,则需要类型注解。例如,此代码:

let a: u8 = return ();
let b: bool = abort 0;
let c: signer = loop ();

let x = return (); // 错误!
//  ^ 无法推断此类型。尝试添加注解
let y = abort 0; // 错误!
//  ^ 无法推断此类型。尝试添加注解
let z = loop (); // 错误!
//  ^ 无法推断此类型。尝试添加注解

为此代码添加类型注解将会暴露其他关于无效代码或未使用的局部变量的错误,但这个例子对于理解这个问题仍然有帮助。

使用元组的多个声明

let 可以使用元组一次引入多个局部变量。括号内声明的局部变量初始化为元组中的相应值。

let () = ();
let (x0, x1) = (0, 1);
let (y0, y1, y2) = (0, 1, 2);
let (z0, z1, z2, z3) = (0, 1, 2, 3);

表达式的类型必须完全匹配元组模式的元数。

let (x, y) = (0, 1, 2); // 错误!
let (x, y, z, q) = (0, 1, 2); // 错误!

不能在单个 let 中声明多个具有相同名称的局部变量。

let (x, x) = 0; // 错误!

声明的局部变量的可变性可以混合。

let (mut x, y) = (0, 1);
x = 1;

使用结构体的多个声明

let 还可以在解构(或匹配)结构体时一次引入多个局部变量。在这种形式中,let 创建一组局部变量,这些变量被初始化为结构体字段的值。语法如下所示:

public struct T { f1: u64, f2: u64 }
let T { f1: local1, f2: local2 } = T { f1: 1, f2: 2 };
// local1: u64
// local2: u64

类似地,对于位置结构体:

public struct P(u64, u64)

let P (local1, local2) = P (1, 2);
// local1: u64
// local2: u64

下面是一个更复杂的例子:

module 0x42::example {
    public struct X(u64);
    public struct Y { x1: X, x2: X }

    fun new_x(): X {
        X(1)
    }

    fun example() {
        let Y { x1: X(f), x2 } = Y { x1: new_x(), x2: new_x() };
        assert!(f + x2.0 == 2, 42);

        let Y { x1: X(f1), x2: X(f2) } = Y { x1: new_x(), x2: new_x() };
        assert!(f1 + f2 == 2, 42);
    }
}

结构体的字段可以起到双重作用,既标识要绑定的字段,又标识变量的名称。这有时被称为捣鬼(punning)。

let Y { x1, x2 } = e;

等价于:

let Y { x1: x1, x2: x2 } = e;

如元组所示,不能在单个 let 中声明多个具有相同名称的局部变量。

let Y { x1: x, x2: x } = e; // 错误!

同样地,声明的局部变量的可变性可以混合。

let Y { x1: mut x1, x2 } = e;

此外,可变性注解可以应用于捣鬼字段。给出等价的例子:

let Y { mut x1, x2 } = e;

针对引用的解构

在上述结构体的例子中,let 中绑定的值被移动,销毁了结构体值并绑定其字段。

public struct T { f1: u64, f2: u64 }
let T { f1: local1, f2: local2 } = T { f1: 1, f2: 2 };
// local1: u64
// local2: u64

在这种情况下,结构体值 T { f1: 1, f2: 2 }let 之后不再存在。

如果您希望不移动和销毁结构体值,可以借用它的每个字段。例如:

let t = T { f1: 1, f2: 2 };
let T { f1: local1, f2: local2 } = &t;
// local1: &u64
// local2: &u64

同样地,对于可变引用:

let mut t = T { f1: 1, f2: 2 };
let T { f1: local1, f2: local2 } = &mut t;
// local1: &mut u64
// local2: &mut u64

这种行为也适用于嵌套结构体。

module 0x42::example {
    public struct X(u64);
    public struct Y { x1: X, x2: X }

    fun new_x(): X {
        X(1)
    }

    fun example() {
        let mut y = Y { x1: new_x(), x2: new_x() };

        let Y { x1: X(f), x2 } = &y;
        assert!(*f + x2.0 == 2, 42);

        let Y { x1: X(f1), x2: X(f2) } = &mut y;
        *f1 = *f1 + 1;
        *f2 = *f2 + 1;
        assert!(*f1 + *f2 == 4, 42);
    }
}

忽略数值

let 绑定中,有时候忽略一些数值是很有帮助的。以 _ 开头的局部变量会被忽略,不会引入新的变量。

fun three(): (u64, u64, u64) {
    (0, 1, 2)
}
let (x1, _, z1) = three();
let (x2, _y, z2) = three();
assert!(x1 + z1 == x2 + z2, 42);

有时候这是必要的,因为编译器会对未使用的局部变量发出警告。

let (x1, y, z1) = three(); // 警告!
//       ^ 未使用的局部变量 'y'

通用的 let 语法

所有 let 语句中的不同结构都可以结合在一起!由此得出了 let 语句的通用语法:

let-bindinglet pattern-or-list type-annotationopt initializeropt
pattern-or-listpattern | ( pattern-list )
pattern-listpattern ,opt | pattern , pattern-list
type-annotation: type
initializer= expression

引入绑定的项目的通用术语是 pattern。模式既用于解构数据(可能是递归的),也用于引入绑定。模式的语法如下:

patternlocal-variable | struct-type { field-binding-list }
field-binding-listfield-binding ,opt | field-binding , field-binding-list
field-bindingfield | field : pattern

使用此语法的几个具体示例:

    let (x, y): (u64, u64) = (0, 1);
//       ^                           local-variable
//       ^                           pattern
//          ^                        local-variable
//          ^                        pattern
//          ^                        pattern-list
//       ^^^^                        pattern-list
//      ^^^^^^                       pattern-or-list
//            ^^^^^^^^^^^^           type-annotation
//                         ^^^^^^^^  initializer
//  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ let-binding

    let Foo { f, g: x } = Foo { f: 0, g: 1 };
//      ^^^                                    struct-type
//            ^                                field
//            ^                                field-binding
//               ^                             field
//                  ^                          local-variable
//                  ^                          pattern
//               ^^^^                          field-binding
//            ^^^^^^^                          field-binding-list
//      ^^^^^^^^^^^^^^^                        pattern
//      ^^^^^^^^^^^^^^^                        pattern-or-list
//                      ^^^^^^^^^^^^^^^^^^^^   initializer
//  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ let-binding

变更

赋值

在引入局部变量后(通过 let 或作为函数参数),可以通过赋值修改 mut 局部变量:

x = e

let 绑定不同,赋值是表达式。在某些语言中,赋值返回被赋的值,但在 Move 中,任何赋值的类型始终为 ()

(x = e: ())

实际上,赋值作为表达式意味着它们可以在不使用大括号({...})添加新表达式块的情况下使用。

let x;
if (cond) x = 1 else x = 2;

赋值使用与 let 绑定类似的模式语法方案,但不包含 mut

module 0x42::example {
    public struct X { f: u64 }

    fun new_x(): X {
        X { f: 1 }
    }

    // 注意:此示例会有关于未使用变量和赋值的警告。
    fun example() {
       let (mut x, mut y, mut f, mut g) = (0, 0, 0, 0);

       (X { f }, X { f: x }) = (new_x(), new_x());
       assert!(f + x == 2, 42);

       (x, y, f, _, g) = (0, 0, 0, 0, 0);
    }
}

注意,局部变量只能有一种类型,因此在赋值之间不能改变局部变量的类型。

let mut x;
x = 0;
x = false; // 错误!

通过引用进行修改

除了直接使用赋值修改局部变量外,还可以通过可变引用 &mut 修改 mut 局部变量。

let mut x = 0;
let r = &mut x;
*r = 1;
assert!(x == 1, 42);

这特别有用,如果:

(1) 你想根据某些条件修改不同的变量。

let mut x = 0;
let mut y = 1;
let r = if (cond) &mut x else &mut y;
*r = *r + 1;

(2) 你希望另一个函数修改你的局部值。

let mut x = 0;
modify_ref(&mut x);

这种修改方式也适用于修改结构体和向量!

let mut v = vector[];
vector::push_back(&mut v, 100);
assert!(*vector::borrow(&v, 0) == 100, 42);

更多细节请参阅 Move references

作用域

任何用 let 声明的局部变量,在其后的任何表达式中均可用,在该作用域内有效。作用域由表达式块 {...} 声明。

局部变量不能在声明的作用域之外使用。

let x = 0;
{
    let y = 1;
};
x + y // 错误!
//  ^ 未绑定的局部变量 'y'

但是,外部作用域的局部变量可以在嵌套作用域中使用。

{
    let x = 0;
    {
        let y = x + 1; // 合法
    }
}

局部变量可以在其可访问的任何作用域中进行修改。该变更将随着局部变量一起生存,不论执行变更的作用域如何。

let mut x = 0;
x = x + 1;
assert!(x == 1, 42);
{
    x = x + 1;
    assert!(x == 2, 42);
};
assert!(x == 2, 42);

表达式块

表达式块是一系列由分号 (;) 分隔的语句。表达式块的结果值是块中最后一个表达式的值。

{ let x = 1; let y = 1; x + y }

在这个例子中,块的结果是 x + y 的值。

语句可以是 let 声明或表达式。请记住,赋值语句 (x = e) 是类型为 () 的表达式。

{ let x; let y = 1; x = 1; x + y }

函数调用是另一种常见的类型为 () 的表达式。修改数据的函数调用通常用作语句。

{ let v = vector[]; vector::push_back(&mut v, 1); v }

这不仅限于 () 类型 --- 任何表达式都可以作为序列中的语句使用!

{
    let x = 0;
    x + 1; // 值被丢弃
    x + 2; // 值被丢弃
    b"hello"; // 值被丢弃
}

但是!如果表达式中包含资源(即没有 drop 能力 的值),你会收到错误消息。这是因为 Move 的类型系统保证任何被丢弃的值都具有 drop 能力。(所有权必须在声明模块内部转移或显式销毁该值。)

{
    let x = 0;
    Coin { value: x }; // 错误!
//  ^^^^^^^^^^^^^^^^^ 未使用值且缺少 `drop` 能力
    x
}

如果在块中没有最终表达式 --- 也就是说,如果有一个尾随的分号 ;,那么会有一个隐式的 单元 ()。同样地,如果表达式块为空,也有一个隐式的单元 () 值。

两者是等效的

{ x = x + 1; 1 / x; }
{ x = x + 1; 1 / x; () }

同样地,这两者也是等效的

{ }
{ () }

表达式块本身是一个表达式,并且可以在任何需要表达式的地方使用。(注意:函数体本身是一个表达式块,但函数体不能被其他表达式替换。)

let my_vector: vector<vector<u8>> = {
    let mut v = vector[];
    vector::push_back(&mut v, b"hello");
    vector::push_back(&mut v, b"goodbye");
    v
};

(在这个例子中不需要类型注释,只是为了清晰起见添加的。)

遮蔽

如果 let 引入的局部变量与作用域中已有的变量同名,那么之前的变量在此作用域后将无法访问。这称为 遮蔽

let x = 0;
assert!(x == 0, 42);

let x = 1; // x 被遮蔽
assert!(x == 1, 42);

当一个局部变量被遮蔽时,它不需要保留之前的类型。

let x = 0;
assert!(x == 0, 42);

let x = b"hello"; // x 被遮蔽
assert!(x == b"hello", 42);

局部变量被遮蔽后,其值仍然存在,但将不再可访问。这点在处理没有 drop 能力 的类型的值时尤为重要,因为该值的所有权必须在函数结束前转移。

module 0x42::example {
    public struct Coin has store { value: u64 }

    fun unused_coin(): Coin {
        let x = Coin { value: 0 }; // 错误!
//          ^ 此局部变量仍包含没有 `drop` 能力的值
        x.value = 1;
        let x = Coin { value: 10 };
        x
//      ^ 返回无效
    }
}

当局部变量在作用域内被遮蔽时,遮蔽仅在该作用域内有效。一旦作用域结束,遮蔽就消失了。

let x = 0;
{
    let x = 1;
    assert!(x == 1, 42);
};
assert!(x == 0, 42);

请记住,局部变量在被遮蔽时可以改变类型。

let x = 0;
{
    let x = b"hello";
    assert!(x = b"hello", 42);
};
assert!(x == 0, 42);

Move 和 Copy

在 Move 中,所有局部变量可以通过 movecopy 两种方式使用。如果没有明确指定其中一种,Move 编译器可以推断出应该使用 copy 还是 move。这意味着在上述所有例子中,编译器会插入 movecopy

从其他编程语言过来的人会更熟悉 copy,因为它会创建变量内部值的新副本以供表达式使用。使用 copy,局部变量可以多次使用。

let x = 0;
let y = copy x + 1;
let z = copy x + 2;

任何具有 copy 能力 的值都可以以此方式复制,并且除非指定了 move,否则会自动复制。

move 将值从局部变量中取出,而不复制数据。一旦发生 move,该局部变量将不再可用,即使值的类型具有 copy 能力 也是如此。

let x = 1;
let y = move x + 1;
//      ------ 局部变量在此处被移动
let z = move x + 2; // 错误!
//      ^^^^^^ 无效使用局部变量 'x'
y + z

安全性

Move 的类型系统将阻止在值移动后继续使用该值。这与let声明中描述的安全检查相同,防止局部变量在分配值之前被使用。

推断

如上所述,如果未指定 copymove,Move 编译器会推断出应该使用 copy 还是 move。该算法非常简单:

  • 任何具有 copy 能力 的值被视为 copy
  • 任何引用(可变 &mut 和不可变 &)被视为 copy
    • 除了在特殊情况下,为了可预测的借用检查错误而被视为 move。这将在引用不再使用时发生。
  • 其他任何值被视为 move

给定以下结构体

public struct Foo has copy, drop, store { f: u64 }
public struct Coin has store { value: u64 }

我们有以下示例

let s = b"hello";
let foo = Foo { f: 0 };
let coin = Coin { value: 0 };
let coins = vector[Coin { value: 0 }, Coin { value: 0 }];

let s2 = s; // copy
let foo2 = foo; // copy
let coin2 = coin; // move
let coins2 = coin; // move

let x = 0;
let b = false;
let addr = @0x42;
let x_ref = &x;
let coin_ref = &mut coin2;

let x2 = x; // copy
let b2 = b; // copy
let addr2 = @0x42; // copy
let x_ref2 = x_ref; // copy
let coin_ref2 = coin_ref; // copy

Equality(相等性)

Move语言支持两种相等性操作 ==!=

操作

语法操作描述
==等于如果两个操作数具有相同的值,则返回 true,否则返回 false
!=不等于如果两个操作数具有不同的值,则返回 true,否则返回 false

类型

==!= 操作只有在两个操作数具有相同类型时才能使用。

0 == 0; // `true`
1u128 == 2u128; // `false`
b"hello" != x"00"; // `true`

相等性和不相等性操作也适用于所有用户定义的类型!

module 0x42::example {
    public struct S has copy, drop { f: u64, s: vector<u8> }

    fun always_true(): bool {
        let s = S { f: 0, s: b"" };
        s == s
    }

    fun always_false(): bool {
        let s = S { f: 0, s: b"" };
        s != s
    }
}

如果操作数具有不同的类型,则会出现类型检查错误:

1u8 == 1u128; // 错误!
//     ^^^^^ 需要类型为 'u8' 的参数
b"" != 0; // 错误!
//     ^ 需要类型为 'vector<u8>' 的参数

引用类型比较

在比较引用时,引用的类型(不可变或可变)并不重要。这意味着可以将一个不可变的 & 引用与相同底层类型的可变 &mut 引用进行比较。

let i = &0;
let m = &mut 1;

i == m; // `false`
m == i; // `false`
m == m; // `true`
i == i; // `true`

以上代码等同于在需要时对每个可变引用进行显式冻结:

let i = &0;
let m = &mut 1;

i == freeze(m); // `false`
freeze(m) == i; // `false`
m == m; // `true`
i == i; // `true`

但是,底层类型必须是相同的类型:

let i = &0;
let s = &b"";

i == s; // 错误!
//   ^ 需要类型为 '&u64' 的参数

自动借用

从 Move 2024 版本开始,如果一个操作数是引用而另一个不是,==!= 操作符会自动将其借用。这意味着以下代码可以正常工作而不会出现任何错误:

let r = &0;

// 在所有情况下,`0` 都会自动作为 `&0` 进行借用
r == 0; // `true`
0 == r; // `true`
r != 0; // `false`
0 != r; // `false`

这种自动借用始终是不可变借用。

限制

==!= 操作符在比较时会消耗值。因此,类型系统要求类型必须具有 drop 能力。需要注意的是,如果没有 drop 能力,所有权必须在函数结束时转移,并且此类值只能在其声明模块内显式销毁。如果直接在相等性 == 或不相等性 != 中使用它们,将会销毁该值,从而破坏 drop 能力 的安全性保证!

module 0x42::example {
    public struct Coin has store { value: u64 }
    fun invalid(c1: Coin, c2: Coin) {
        c1 == c2 // 错误!
//      ^^    ^^ 这些资产将被销毁!
    }
}

但是,程序员可以始终先借用该值而不是直接比较该值,并且引用类型具有 drop。例如:

module 0x42::example {
    public struct Coin has store { value: u64 }
    fun swap_if_equal(c1: Coin, c2: Coin): (Coin, Coin) {
        let are_equal = &c1 == c2; // 合法,注意 `c2` 会自动被借用
        if (are_equal) (c2, c1) else (c1, c2)
    }
}

避免额外的拷贝

虽然程序员可以比较任何具有 drop 的类型的值,但通常应该通过引用进行比较,以避免昂贵的拷贝操作。

let v1: vector<u8> = function_that_returns_vector();
let v2: vector<u8> = function_that_returns_vector();
assert!(copy v1 == copy v2, 42);
//      ^^^^       ^^^^
use_two_vectors(v1, v2);

let s1: Foo = function_that_returns_large_struct();
let s2: Foo = function_that_returns_large_struct();
assert!(copy s1 == copy s2, 42);
//      ^^^^       ^^^^
use_two_foos(s1, s2);

此代码是完全可接受的(假设 Foo 具有 drop),但不是高效的。可以移除高亮的拷贝操作,并用借用代替:

let v1: vector<u8> = function_that_returns_vector();
let v2: vector<u8> = function_that_returns_vector();
assert!(&v1 == &v2, 42);
//      ^      ^
use_two_vectors(v1, v2);

let s1: Foo = function_that_returns_large_struct();
let s2: Foo = function_that_returns_large_struct();
assert!(&s1 == &s2, 42);
//      ^      ^
use_two_foos(s1, s2);

== 本身的效率不变,但是拷贝操作被移除,因此程序更加高效。

Abort and Assert

returnabort 是两种控制流结构,用于结束执行,分别适用于当前函数和整个交易。

更多关于return的信息可以在链接部分找到

abort

abort 是一个表达式,接受一个参数:类型为 u64中断码。例如:

abort 42

abort 表达式会停止当前函数的执行并回滚当前交易对状态所做的所有更改(注意,这一保证必须由 Move 的具体部署适配器来确保)。没有机制可以“捕获”或以其他方式处理 abort

幸运的是,在 Move 中,交易是全有或全无的,这意味着只有当交易成功时,任何对存储的更改才会全部生效。对于 Sui,这意味着不会修改任何对象。

由于这种事务性提交更改的承诺,在 abort 之后不需要担心回退更改。虽然这种方法在灵活性上有所欠缺,但它非常简单且可预测。

return 类似,abort 适用于在某些条件无法满足时退出控制流。

在这个例子中,函数会从向量中弹出两个项目,但如果向量没有两个项目,则会提前中断

use std::vector;

fun pop_twice<T>(v: &mut vector<T>): (T, T) {
    if (vector::length(v) < 2) abort 42;
    (vector::pop_back(v), vector::pop_back(v))
}

在控制流结构中,abort 更有用。例如,这个函数检查向量中的所有数字是否小于指定的 bound,否则中断

use std::vector;
fun check_vec(v: &vector<u64>, bound: u64) {
    let i = 0;
    let n = vector::length(v);
    while (i < n) {
        let cur = *vector::borrow(v, i);
        if (cur > bound) abort 42;
        i = i + 1;
    }
}

assert

assert 是由 Move 编译器提供的内置宏操作。它接受两个参数,一个类型为 bool 的条件和一个类型为 u64 的代码

assert!(condition: bool, code: u64)

由于该操作是一个宏,必须使用 ! 来调用。这是为了表明 assert 的参数是按表达式调用的。换句话说,assert 不是一个普通函数,在字节码级别不存在。它在编译器内被替换为

if (condition) () else abort code

assert 比单独使用 abort 更常用。上面的 abort 示例可以使用 assert 重写

use std::vector;
fun pop_twice<T>(v: &mut vector<T>): (T, T) {
    assert!(vector::length(v) >= 2, 42); // 现在使用 'assert'
    (vector::pop_back(v), vector::pop_back(v))
}

use std::vector;
fun check_vec(v: &vector<u64>, bound: u64) {
    let i = 0;
    let n = vector::length(v);
    while (i < n) {
        let cur = *vector::borrow(v, i);
        assert!(cur <= bound, 42); // 现在使用 'assert'
        i = i + 1;
    }
}

注意,由于该操作被替换为这个 if-else,因此 code 的参数并不总是被评估。例如:

assert!(true, 1 / 0)

不会导致算术错误,它相当于

if (true) () else (1 / 0)

因此算术表达式从未被评估!

Move 虚拟机中的中断码

使用 abort 时,了解 u64 代码将如何被虚拟机使用很重要。

通常,在成功执行后,Move 虚拟机及其具体部署的适配器会确定对存储所做的更改。

如果遇到 abort,虚拟机将改为指示错误。错误信息中将包含两条信息:

  • 产生中断的模块(包/地址值和模块名称)
  • 中断码。

例如

module 0x2::example {
    public fun aborts() {
        abort 42
    }
}

module 0x3::invoker {
    public fun always_aborts() {
        0x2::example::aborts()
    }
}

如果一个交易,比如上面的函数 always_aborts,调用 0x2::example::aborts,虚拟机会产生一个错误,指示模块 0x2::example 和代码 42

这对于在模块内将多个中断分组在一起非常有用。

在这个例子中,模块有两个单独的错误码,分别在多个函数中使用

module 0x42::example {

    use std::vector;

    const EEmptyVector: u64 = 0;
    const EIndexOutOfBounds: u64 = 1;

    // 将 i 移动到 j,将 j 移动到 k,将 k 移动到 i
    public fun rotate_three<T>(v: &mut vector<T>, i: u64, j: u64, k: u64) {
        let n = vector::length(v);
        assert!(n > 0, EEmptyVector);
        assert!(i < n, EIndexOutOfBounds);
        assert!(j < n, EIndexOutOfBounds);
        assert!(k < n, EIndexOutOfBounds);

        vector::swap(v, i, k);
        vector::swap(v, j, k);
    }

    public fun remove_twice<T>(v: &mut vector<T>, i: u64, j: u64): (T, T) {
        let n = vector::length(v);
        assert!(n > 0, EEmptyVector);
        assert!(i < n, EIndexOutOfBounds);
        assert!(j < n, EIndexOutOfBounds);
        assert!(i > j, EIndexOutOfBounds);

        (vector::remove<T>(v, i), vector::remove<T>(v, j))
    }
}

abort 的类型

abort i 表达式可以具有任何类型!这是因为这两种结构都会打破正常的控制流,所以它们永远不需要评估为该类型的值。

以下代码虽然没什么用处,但它们会通过类型检查

let y: address = abort 0;

这种行为在某些情况下非常有用,例如当你有一个分支指令在某些分支上生成一个值,而在其他分支上则不生成。例如:

let b =
    if (x == 0) false
    else if (x == 1) true
    else abort 42;
//       ^^^^^^^^ `abort 42` 的类型是 `bool`

控制流

Move 提供了多种基于布尔表达式的控制流结构,包括常见的编程结构如 if 表达式、whilefor 循环,以及高级控制流结构,包括带标签的循环和可逃逸的命名块。它还支持基于结构模式匹配的更复杂的结构。

条件if表达式

if表达式指定只有在某个条件为真时才会评估一些代码。 例如:

if (x > 5) x = x - 5

条件必须是一个bool类型的表达式。

if表达式可以可选地包含一个else子句,用于指定当条件为假时要评估的另一个表达式。

if (y <= 10) y = y + 1 else y = 10

"true"分支或"false"分支将被评估,但不会同时评估两者。任一分支可以是单个表达式或表达式块。

条件表达式可以生成值,以使if表达式具有结果。

let z = if (x < 100) x else 100;

true分支和false分支的表达式必须具有兼容的类型。例如:

// x 和 y 必须是 u64 整数
let maximum: u64 = if (x > y) x else y;

// 错误!分支类型不同
let z = if (maximum < 10) 10u8 else 100u64;

// 错误!分支类型不同,因为默认的false分支是()而不是u64
if (maximum >= 10) maximum;

如果未指定else子句,则false分支默认为单元值。以下两种写法是等价的:

if (条件) true分支 // 默认隐含:else ()
if (条件) true分支 else ()

通常,if表达式与表达式块一起使用。

let maximum = if (x > y) x else y;
if (maximum < 10) {
    x = x + 10;
    y = y + 10;
} else if (x >= 10 && y >= 10) {
    x = x - 10;
    y = y - 10;
}

条件语句的语法

if-expressionif ( expression ) expression else-clauseopt > else-clauseelse expression

Move 中的循环结构

许多程序需要对值进行迭代,Move 提供了 whileloop 形式来让你编写这样的代码。此外,你还可以使用 break(退出循环)和 continue(跳过本次迭代的剩余部分并返回到控制流结构的顶部)在执行过程中修改这些循环的控制流。

while 循环

while 构造重复执行循环体(一个 unit 类型的表达式),直到条件(一个 bool 类型的表达式)计算结果为 false

下面是一个简单的 while 循环示例,它计算从 1n 的数字之和:

fun sum(n: u64): u64 {
    let mut sum = 0;
    let mut i = 1;
    while (i <= n) {
        sum = sum + i;
        i = i + 1
    };

    sum
}

无限 while 循环也是允许的:

fun foo() {
    while (true) { }
}

while 循环中使用 break

在 Move 中,while 循环可以使用 break 提前退出。例如,假设我们正在查找向量中的某个值的位置,并希望在找到它时退出:

fun find_position(values: &vector<u64>, target_value: u64): Option<u64> {
    let size = vector::length(values);
    let mut i = 0;
    let mut found = false;

    while (i < size) {
        if (vector::borrow(values, i) == &target_value) {
            found = true;
            break
        };
        i = i + 1
    };

    if (found) {
        Option::Some(i)
    } else {
        Option::None
    }
}

在这里,如果借用的向量值等于我们的目标值,我们将 found 标志设置为 true,然后调用 break,这将导致程序退出循环。

最后,请注意 while 循环的 break 不能带值:while 循环始终返回 unit 类型 (),因此 break 也是。

while 循环中使用 continue

break 类似,Move 的 while 循环可以调用 continue 跳过部分循环体。这允许我们在条件不满足时跳过部分计算,例如以下示例:

fun sum_even(values: &vector<u64>): u64 {
    let size = vector::length(values);
    let mut i = 0;
    let mut even_sum = 0;

    while (i < size) {
        let number = *vector::borrow(values, i);
        i = i + 1;
        if (number % 2 == 1) continue;
        even_sum = even_sum + number;
    };
    even_sum
}

此代码将遍历提供的向量。对于每个条目,如果该条目是偶数,它将其加到 even_sum 中。如果不是,则调用 continue,跳过求和操作并返回到 while 循环条件检查。

loop 表达式

loop 表达式重复执行循环体(一个类型为 () 的表达式),直到遇到 break

fun sum(n: u64): u64 {
    let mut sum = 0;
    let mut i = 1;

    loop {
       i = i + 1;
       if (i >= n) break;
       sum = sum + i;
    };

    sum
}

没有 break 的话,循环将永远继续。在下面的示例中,由于 loop 没有 break,程序将永远运行:

fun foo() {
    let mut i = 0;
    loop { i = i + 1 }
}

下面是一个使用 loop 编写 sum 函数的示例:

fun sum(n: u64): u64 {
    let sum = 0;
    let i = 0;
    loop {
        i = i + 1;
        if (i > n) break;
        sum = sum + i
    };

    sum
}

loop 中使用带值的 break

与始终返回 ()while 循环不同,loop 可以使用 break 返回一个值。这样做时,整个 loop 表达式计算结果为该类型的值。例如,我们可以使用 loopbreak 重写上面的 find_position,在找到目标值时立即返回索引:

fun find_position(values: &vector<u64>, target_value: u64): Option<u64> {
    let size = vector::length(values);
    let mut i = 0;

    loop {
        if (vector::borrow(values, i) == &target_value) {
            break Option::Some(i)
        } else if (i >= size) {
            break Option::None
        };
        i = i + 1;
    }
}

此循环将以一个 Option 结果结束,并作为函数体的最后一个表达式,生成该值作为最终的函数结果。

loop 表达式中使用 continue

如你所料,continue 也可以在 loop 中使用。以下是之前使用 loopbreakcontinue 重写的 sum_even 函数。

fun sum_even(values: &vector<u64>): u64 {
    let size = vector::length(values);
    let mut i = 0;
    let mut even_sum = 0;

    loop {
        if (i >= size) break;
        let number = *vector::borrow(values, i);
        i = i + 1;
        if (number % 2 == 1) continue;
        even_sum = even_sum + number;
    };
    even_sum
}

whileloop 的类型

在 Move 中,循环是类型化表达式。while 表达式始终具有 () 类型。

let () = while (i < 10) { i = i + 1 };

如果一个 loop 包含一个 break,该表达式的类型为 break 的类型。没有值的 break 具有 unit 类型 ()

(loop { if (i < 10) i = i + 1 else break }: ());
let () = loop { if (i < 10) i = i + 1 else break };

let x: u64 = loop { if (i < 10) i = i + 1 else break 5 };
let x: u64 = loop { if (i < 10) { i = i + 1; continue} else break 5 };

此外,如果一个 loop 包含多个 break,它们必须全部返回相同的类型:

// 无效 -- 第一个 break 返回 (),第二个返回 5
let x: u64 = loop { if (i < 10) break else break 5 };

如果 loop 没有 breakloop 可以具有任何类型,就像 returnabortbreakcontinue 一样。

(loop (): u64);
(loop (): address);
(loop (): &vector<vector<u8>>);

如果你需要更精确的控制流,例如跳出嵌套循环,下一章将介绍 Move 中的标签控制流的使用。

带标签的控制流

Move支持带标签的控制流,允许您在函数中定义特定标签并将控制权转移到这些标签。例如,我们可以嵌套两个循环,并使用带有这些标签的breakcontinue来精确指定控制流。您可以在任何loopwhile形式前加上'label:形式,以允许直接在那里中断或继续。

为了演示这种行为,我们来考虑一个函数,它接受嵌套的数字向量(即vector<vector<u64>>)并根据某个阈值进行求和,其行为如下:

  • 如果所有数字的总和低于阈值,返回该总和。
  • 如果将一个数字加到当前总和会超过阈值,则返回当前总和。

我们可以通过将向量的向量作为嵌套循环进行迭代,并标记外部循环来编写这个函数。如果内部循环中的任何加法会使我们超过阈值,我们可以使用带有外部标签的break来一次性跳出两个循环:

fun sum_until_threshold(input: &vector<vector<u64>>, threshold: u64): u64 {
    let mut sum = 0;
    let mut i = 0;
    let input_size = vector::length(vec);

    'outer: loop {
        // 跳出到outer,因为它是最近的外围循环
        if (i >= input_size) break sum;

        let vec = vector::borrow(input, i);
        let size = vector::length(vec);
        let mut j = 0;

        while (j < size) {
            let v_entry = *vector::borrow(vec, j);
            if (sum + v_entry < threshold) {
                sum = sum + v_entry;
            } else {
                // 我们看到的下一个元素会突破阈值,
                // 所以我们返回当前的总和
                break 'outer sum
            };
            j = j + 1;
        };
        i = i + 1;
    }
}

这些标签也可以用于嵌套的循环形式,在更大的代码块中提供精确的控制。例如,如果我们正在处理一个大表,其中每个条目都需要迭代,可能会看到我们继续内部或外部循环,我们可以使用标签来表达该代码:

'outer: loop {
    ...
    'inner: while (cond) {
        ...
        if (cond0) { break 'outer value };
        ...
        if (cond1) { continue 'inner }
        else if (cond2) { continue 'outer };
        ...
    }
    ...
}

模式匹配

match 表达式是一种强大的控制结构,允许你将一个值与一系列模式进行比较,然后根据首先匹配的模式执行代码。模式可以是简单的字面量,也可以是复杂的、嵌套的结构体和枚举定义。与基于 bool 类型测试表达式的 if 表达式不同,match 表达式操作任何类型的值并选择多个分支之一。

match 表达式可以匹配 Move 值以及可变或不可变引用,并相应地绑定子模式。

例如:

fun run(x: u64): u64 {
    match (x) {
        1 => 2,
        2 => 3,
        x => x,
    }
}

run(1); // 返回 2
run(2); // 返回 3
run(3); // 返回 3
run(0); // 返回 0

match 语法

match 包含一个表达式和一个非空的 match 分支 列表,用逗号分隔。

每个 match 分支由一个模式 (p)、一个可选的守卫 (if (g),其中 g 是一个 bool 类型的表达式)、一个箭头 (=>) 和一个分支表达式 (e) 组成,当模式匹配时执行。例如,

match (expression) {
    pattern1 if (guard_expression) => expression1,
    pattern2 => expression2,
    pattern3 => { expression3, expression4, ... },
}

match 分支按顺序从上到下检查,第一个匹配的模式(如果存在,则匹配的守卫表达式为 true)将被执行。

请注意,match 中的分支必须是穷尽的,意味着匹配的类型的每一个可能的值都必须由 match 中的一个模式覆盖。如果分支不穷尽,编译器将抛出错误。

模式语法

一个模式与一个值匹配,如果该值等于模式,其中变量和通配符(例如 xy_..)与任何值“相等”。

模式用于匹配值。模式可以是:

模式描述
字面量字面量值,例如 1true@0x1
常量常量值,例如 MyConstant
变量变量,例如 xyz
通配符通配符,例如 _
构造器构造器模式,例如 MyStruct { x, y }MyEnum::Variant(x)
@ 模式at 模式,例如 x @ MyEnum::Variant(..)
或模式或模式,例如 MyEnum::Variant(..) \| MyEnum::OtherVariant(..)
多元通配符多元通配符,例如 MyEnum::Variant(..)
可变绑定可变绑定模式,例如 mut x

Move 中的模式具有以下语法:

pattern = <literal>
        | <constant>
        | <variable>
        | _
        | C { <variable> : inner-pattern ["," <variable> : inner-pattern]* } // C 是结构体或枚举变体
        | C ( inner-pattern ["," inner-pattern]* ... )                       // C 是结构体或枚举变体
        | C                                                                  // C 是枚举变体
        | <variable> @ top-level-pattern
        | pattern | pattern
        | mut <variable>
inner-pattern = pattern
              | ..     // 多元通配符

一些模式示例:

// 字面量模式
1

// 常量模式
MyConstant

// 变量模式
x

// 通配符模式
_

// 构造器模式,匹配 `MyEnum::Variant`,字段为 `1` 和 `true`
MyEnum::Variant(1, true)

// 构造器模式,匹配 `MyEnum::Variant`,字段为 `1` 并将第二个字段的值绑定到 `x`
MyEnum::Variant(1, x)

// 多元通配符模式,匹配 `MyEnum::Variant` 变体中的多个字段
MyEnum::Variant(..)

// 构造器模式,匹配 `MyStruct` 的 `x` 字段并将 `y` 字段绑定到 `other_variable`
MyStruct { x, y: other_variable }

// at 模式,匹配 `MyEnum::Variant` 并将整个值绑定到 `x`
x @ MyEnum::Variant(..)

// 或模式,匹配 `MyEnum::Variant` 或 `MyEnum::OtherVariant`
MyEnum::Variant(..) | MyEnum::OtherVariant(..)

// 与上述或模式相同,但使用显式通配符
MyEnum::Variant(_, _) | MyEnum::OtherVariant(_, _)

// 或模式,匹配 `MyEnum::Variant` 或 `MyEnum::OtherVariant`,并将 u64 字段绑定到 `x`
MyEnum::Variant(x, _) | MyEnum::OtherVariant(_, x)

// 构造器模式,匹配 `OtherEnum::V` 并且内部 `MyEnum` 是 `MyEnum::Variant`
OtherEnum::V(MyEnum::Variant(..))

模式和变量

包含变量的模式将变量绑定到匹配的值或值的子组件。这些变量可以在任何匹配守卫表达式中使用,也可以在匹配分支的右侧使用。例如:

public struct Wrapper(u64)

fun add_under_wrapper_unless_equal(wrapper: Wrapper, x: u64): u64 {
    match (wrapper) {
        Wrapper(y) if (y == x) => Wrapper(y),
        Wrapper(y) => y + x,
    }
}
add_under_wrapper_unless_equal(Wrapper(1), 2); // 返回 Wrapper(3)
add_under_wrapper_unless_equal(Wrapper(2), 3); // 返回 Wrapper(5)
add_under_wrapper_unless_equal(Wrapper(3), 3); // 返回 Wrapper(3)

组合模式

模式可以嵌套,但也可以使用或运算符 (|) 组合模式。例如,p1 | p2 成功匹配如果模式 p1p2 中的任何一个匹配。这种模式可以出现在任何地方——作为顶层模式或另一个模式中的子模式。

public enum MyEnum has drop {
    Variant(u64, bool),
    OtherVariant(bool, u64),
}

fun test_or_pattern(x: u64): u64 {
    match (x) {
        MyEnum::Variant(1 | 2 | 3, true) | MyEnum::OtherVariant(true, 1 | 2 | 3) => 1,
        MyEnum::Variant(8, true) | MyEnum::OtherVariant(_, 6 | 7) => 2,
        _ => 3,
    }
}

test_or_pattern(MyEnum::Variant(3, true)); // 返回 1
test_or_pattern(MyEnum::OtherVariant(true, 2)); // 返回 1
test_or_pattern(MyEnum::Variant(8, true)); // 返回 2
test_or_pattern(MyEnum::OtherVariant(false, 7)); // 返回 2
test_or_pattern(MyEnum::OtherVariant(false, 80)); // 返回 3

某些模式的限制

mut.. 模式在使用时有特定的条件,如 特定模式的限制 中所述。大体上,mut 修饰符只能用于变量模式,.. 模式只能在构造器模式中使用一次——不能作为顶层模式使用。

以下是 .. 模式的 无效 用法,因为它用作顶层模式:

match (x) {
    .. => 1,
    // 错误: `..` 模式只能在构造器模式中使用
}

match (x) {
    MyStruct(.., ..) => 1,
    // 错误:    ^^  `..` 模式只能在构造器模式中使用一次
}

模式类型

模式不是表达式,但它们仍然是类型化的。这意味着模式的类型必须与匹配值的类型匹配。例如,模式 1 具有整数类型,模式 MyEnum::Variant(1, true) 具有 MyEnum 类型,模式 MyStruct { x, y } 具有 MyStruct 类型,而 OtherStruct<bool> { x: true, y: 1} 具有 OtherStruct<bool> 类型。如果尝试匹配类型与模式类型不同的表达式,将导致类型错误。例如:

match (1) {
    // `true` 字面量模式是 `bool` 类型,因此这是一个类型错误。
    true => 1,
    // 类型错误: 预期类型 u64,找到 bool
    _ => 2,
}

同样,以下也会导致类型错误,因为 MyEnumMyStruct 是不同的类型:

match (MyStruct { x: 0, y: 0 }) {
    MyEnum::Variant(..) => 1,
    // TYPE ERROR: expected type MyEnum, found MyStruct
}

匹配

在深入探讨模式匹配的具体细节以及一个值与模式“匹配”意味着什么之前,让我们通过几个示例来提供一个直观的理解。

fun test_lit(x: u64): u8 {
    match (x) {
        1 => 2,
        2 => 3,
        _ => 4,
    }
}
test_lit(1); // 返回 2
test_lit(2); // 返回 3
test_lit(3); // 返回 4
test_lit(10); // 返回 4

fun test_var(x: u64): u64 {
    match (x) {
        y => y,
    }
}
test_var(1); // 返回 1
test_var(2); // 返回 2
test_var(3); // 返回 3
...

const MyConstant: u64 = 10;
fun test_constant(x: u64): u64 {
    match (x) {
        MyConstant => 1,
        _ => 2,
    }
}
test_constant(MyConstant); // 返回 1
test_constant(10); // 返回 1
test_constant(20); // 返回 2

fun test_or_pattern(x: u64): u64 {
    match (x) {
        1 | 2 | 3 => 1,
        4 | 5 | 6 => 2,
        _ => 3,
    }
}
test_or_pattern(3); // 返回 1
test_or_pattern(5); // 返回 2
test_or_pattern(70); // 返回 3

fun test_or_at_pattern(x: u64): u64 {
    match (x) {
        x @ (1 | 2 | 3) => x + 1,
        y @ (4 | 5 | 6) => y + 2,
        z => z + 3,
    }
}
test_or_pattern(2); // 返回 3
test_or_pattern(5); // 返回 7
test_or_pattern(70); // 返回 73

从这些示例中最重要的一点是,一个模式匹配一个值如果该值等于该模式,并且通配符/变量模式匹配任何值。这对文字、变量和常量都是如此。例如,在 test_lit 函数中,值 1 匹配模式 1,值 2 匹配模式 2,而值 3 匹配通配符 _。类似地,在 test_var 函数中,值 12 都匹配模式 y

变量 x 匹配(或“等于”)任何值,而通配符 _ 匹配任何值(但只匹配一个值)。或者模式就像一个逻辑 OR,值匹配模式如果它匹配或模式中的任何一个模式,所以 p1 | p2 | p3 应该被解读为“匹配 p1,或 p2,或 p3”。

匹配构造函数

模式匹配包括构造函数模式的概念。这些模式允许你检查并访问结构体和枚举中的深层次内容,是模式匹配最强大的部分之一。构造函数模式与变量绑定结合,允许你通过结构匹配值,并提取你关心的部分以在匹配分支的右侧使用。

看看下面的例子:

public enum MyEnum has drop {
    Variant(u64, bool),
    OtherVariant(bool, u64),
}

fun f(x: MyEnum): u64 {
    match (x) {
        MyEnum::Variant(1, true) => 1,
        MyEnum::OtherVariant(_, 3) => 2,
        MyEnum::Variant(..) => 3,
        MyEnum::OtherVariant(..) => 4,
    }
}
f(MyEnum::Variant(1, true)); // 返回 1
f(MyEnum::Variant(2, true)); // 返回 3
f(MyEnum::OtherVariant(false, 3)); // 返回 2
f(MyEnum::OtherVariant(true, 3)); // 返回 2
f(MyEnum::OtherVariant(true, 2)); // 返回 4

这段代码的意思是“如果 xMyEnum::Variant 并且字段是 1true,则返回 1。如果它是 MyEnum::OtherVariant 并且第一个字段是任何值,第二个字段是 3,则返回 2。如果它是 MyEnum::Variant 并且字段是任何值,则返回 3。最后,如果它是 MyEnum::OtherVariant 并且字段是任何值,则返回 4”。

你还可以嵌套模式。因此,如果你想匹配 1、2 或 10,而不仅仅是匹配前面的 MyEnum::Variant 中的 1,你可以使用 or 模式来实现:

fun f(x: MyEnum): u64 {
    match (x) {
        MyEnum::Variant(1 | 2 | 10, true) => 1,
        MyEnum::OtherVariant(_, 3) => 2,
        MyEnum::Variant(..) => 3,
        MyEnum::OtherVariant(..) => 4,
    }
}
f(MyEnum::Variant(1, true)); // 返回 1
f(MyEnum::Variant(2, true)); // 返回 1
f(MyEnum::Variant(10, true)); // 返回 1
f(MyEnum::Variant(10, false)); // 返回 3

能力约束

此外,匹配绑定受到与Move其他方面相同的能力限制。特别是,如果你试图使用通配符匹配一个没有drop能力的值(非引用),编译器将发出错误信号,因为通配符期望丢弃该值。同样,如果你使用绑定器绑定一个非drop值,它必须在匹配分支的右侧使用。此外,如果你完全解构该值,你就拆解了它,这与drop结构体拆解的语义相匹配。有关drop能力的更多详细信息,请参阅能力部分的drop

public struct NonDrop(u64)

fun drop_nondrop(x: NonDrop) {
    match (x) {
        NonDrop(1) => 1,
        _ => 2
        // 错误:不能在不可丢弃的值上使用通配符匹配
    }
}

fun destructure_nondrop(x: NonDrop) {
    match (x) {
        NonDrop(1) => 1,
        NonDrop(_) => 2
        // 正确!
    }
}

fun use_nondrop(x: NonDrop): NonDrop {
    match (x) {
        NonDrop(1) => NonDrop(8),
        x => x
    }
}

穷尽性

Move中的match表达式必须是_穷尽的_:被匹配类型的每个可能值都必须被匹配分支中的一个模式所覆盖。如果匹配分支序列不是穷尽的,编译器将引发错误。注意,任何带有守卫表达式的分支都不会贡献于匹配穷尽性,因为它可能在运行时匹配失败。

例如,对u8的匹配只有在匹配0到255(包括255)的_每个_数字时才是穷尽的,除非存在通配符或变量模式。同样,对bool的匹配需要匹配truefalse两者,除非存在通配符或变量模式。

对于结构体,因为类型只有一种构造函数,所以只需要匹配一个构造函数,但结构体内的字段也需要被穷尽地匹配。相反,枚举可能定义多个变体,每个变体都必须被匹配(包括任何子字段)才能被视为穷尽匹配。

因为下划线和变量是匹配任何内容的通配符,它们被视为匹配该位置上它们所匹配类型的所有值。此外,多元通配符模式..可用于匹配结构体或枚举变体内的多个值。

看一些_非穷尽_匹配的例子,考虑以下内容:

public enum MyEnum {
    Variant(u64, bool),
    OtherVariant(bool, u64),
}

public struct Pair<T>(T, T)

fun f(x: MyEnum): u8 {
    match (x) {
        MyEnum::Variant(1, true) => 1,
        MyEnum::Variant(_, _) => 1,
        MyEnum::OtherVariant(_, 3) => 2,
        // 错误:不穷尽,因为值`MyEnum::OtherVariant(_, 4)`没有被匹配。
    }
}

fun match_pair_bool(x: Pair<bool>): u8 {
    match (x) {
        Pair(true, true) => 1,
        Pair(true, false) => 1,
        Pair(false, false) => 1,
        // 错误:不穷尽,因为值`Pair(false, true)`没有被匹配。
    }
}

可以通过在匹配分支末尾添加通配符模式,或完全匹配剩余值来使这些例子变得穷尽:

fun f(x: MyEnum): u8 {
    match (x) {
        MyEnum::Variant(1, true) => 1,
        MyEnum::Variant(_, _) => 1,
        MyEnum::OtherVariant(_, 3) => 2,
        // 现在穷尽了,因为这将匹配MyEnum::OtherVariant的所有值
        MyEnum::OtherVariant(..) => 2,
    }
}

fun match_pair_bool(x: Pair<bool>): u8 {
    match (x) {
        Pair(true, true) => 1,
        Pair(true, false) => 1,
        Pair(false, false) => 1,
        // 现在穷尽了,因为这将匹配Pair<bool>的所有值
        Pair(false, true) => 1,
    }
}

守卫

如前所述,你可以通过在模式后添加一个 if 子句来向匹配分支添加守卫。这个守卫将在模式匹配之后但在箭头右侧的表达式求值之前运行。如果守卫表达式求值为 true,则箭头右侧的表达式将被求值;如果求值为 false,则将被视为匹配失败,并检查 match 表达式中的下一个匹配分支。

fun match_with_guard(x: u64): u64 {
    match (x) {
        1 if (false) => 1,
        1 => 2,
        _ => 3,
    }
}

match_with_guard(1); // 返回 2
match_with_guard(0); // 返回 3

守卫表达式可以引用在模式求值期间绑定的变量。然而,请注意,无论模式是如何匹配的,变量在守卫中始终仅作为不可变引用可用——即使变量上有可变性说明符或者模式是按值匹配的。

fun incr(x: &mut u64) {
    *x = *x + 1;
}

fun match_with_guard_incr(x: u64): u64 {
    match (x) {
        x if ({ incr(&mut x); x == 1 }) => 1,
        // 错误:    ^^^ 对不可变值的无效借用
        _ => 2,
    }
}

fun match_with_guard_incr2(x: &mut u64): u64 {
    match (x) {
        x if ({ incr(&mut x); x == 1 }) => 1,
        // 错误:    ^^^ 对不可变值的无效借用
        _ => 2,
    }
}

此外,需要注意的是,任何具有守卫表达式的匹配分支都不会被视为出于穷尽性检查的目的,因为编译器无法静态地评估守卫表达式。

特定模式的限制

在模式中使用 ..mut 模式修饰符时存在一些限制。

可变性使用

mut 修饰符可以放在变量模式上,以指定该变量在匹配分支的右侧表达式中是可变的。请注意,由于 mut 修饰符仅表示变量是可变的,而不是底层数据,因此可以在所有类型的匹配中使用(按值、不可变引用和可变引用)。

请注意,mut 修饰符只能应用于变量,而不能应用于其他类型的模式。

public struct MyStruct(u64);

fun top_level_mut(x: MyStruct) {
    match (x) {
        mut MyStruct(y) => 1,
        // 错误: 不能在非变量模式上使用 mut
    }
}

fun mut_on_immut(x: &MyStruct): u64 {
    match (x) {
        MyStruct(mut y) => {
            y = &(*y + 1);
            *y
        }
    }
}

fun mut_on_value(x: MyStruct): u64 {
    match (x) {
        MyStruct(mut y) =>  {
            *y = *y + 1;
            *y
        },
    }
}

fun mut_on_mut(x: &mut MyStruct): u64 {
    match (x) {
        MyStruct(mut y) =>  {
            *y = *y + 1;
            *y
        },
    }
}

let mut x = MyStruct(1);

mut_on_mut(&mut x); // 返回 2
x.0; // 返回 2

mut_on_immut(&x); // 返回 3
x.0; // 返回 2

mut_on_value(x); // 返回 3

.. 的使用

.. 模式只能在构造函数模式中用作通配符,匹配任意数量的字段——编译器将 .. 展开为在构造函数模式中插入任何缺失字段中的 _。所以 MyStruct(_, _, _) 等同于 MyStruct(..)MyStruct(1, _, _) 等同于 MyStruct(1, ..)。因此,对于 .. 模式的使用有一些限制:

  • 它只能在构造函数模式中使用一次
  • 在位置参数中,它可以在构造函数模式中的开始、中间或结尾使用;
  • 在命名参数中,它只能在构造函数模式的结尾使用;
public struct MyStruct(u64, u64, u64, u64) has drop;

public struct MyStruct2 {
    x: u64,
    y: u64,
    z: u64,
    w: u64,
}

fun wild_match(x: MyStruct) {
    match (x) {
        MyStruct(.., 1) => 1,
        // OK! `..` 模式可以在构造函数模式的开头使用
        MyStruct(1, ..) => 2,
        // OK! `..` 模式可以在构造函数模式的结尾使用
        MyStruct(1, .., 1) => 3,
        // OK! `..` 模式可以在构造函数模式的中间使用
        MyStruct(1, .., 1, 1) => 4,
        MyStruct(..) => 5,
    }
}

fun wild_match2(x: MyStruct2) {
    match (x) {
        MyStruct2 { x: 1, .. } => 1,
        MyStruct2 { x: 1, w: 2 .. } => 2,
        MyStruct2 { .. } => 3,
    }
}

Functions(函数)

函数被声明在模块内部,并定义模块的逻辑和行为。函数可以被重用,可以作为其他函数的调用点或作为执行的入口点。

声明

函数使用 fun 关键字声明,后面跟着函数名、类型参数、参数列表、返回类型和函数体。

<visibility>? <entry>? fun <identifier><[type_parameters: constraint],*>([identifier: type],*): <return_type> <function_body>

例如

fun foo<T1, T2>(x: u64, y: T1, z: T2): (T2, T1, u64) { (z, y, x) }

可见性

模块内的函数默认只能在同一模块内调用。这些内部(有时称为私有)函数不能从其他模块调用或作为入口点。

module a::m {
    fun foo(): u64 { 0 }
    fun calls_foo(): u64 { foo() } // 合法
}

module b::other {
    fun calls_m_foo(): u64 {
        a::m::foo() // 错误!
//      ^^^^^^^^^^^ 'foo' 是 'a::m' 内部的
    }
}

要允许从其他模块访问该函数,函数必须声明为 publicpublic(package)。与可见性相对应,一个 entry 函数可以作为执行的入口点。

public 可见性

public 函数可以被任何模块中的任何函数调用。如下示例所示,public 函数可以被:

  • 同一模块中定义的其他函数调用,
  • 另一个模块中定义的函数调用,或
  • 作为执行的入口点。
module a::m {
    public fun foo(): u64 { 0 }
    fun calls_foo(): u64 { foo() } // 合法
}

module b::other {
    fun calls_m_foo(): u64 {
        a::m::foo() // 合法
    }
}

有关入口点执行的更多详细信息,请参阅下面的章节

public(package) 可见性

public(package) 可见性修饰符是 public 修饰符的一种更受限制的形式,用于更精确地控制函数的使用范围。public(package) 函数可以被:

  • 同一模块中定义的其他函数调用,
  • 同一包(同一地址)中定义的其他函数调用。
module a::m {
    public(package) fun foo(): u64 { 0 }
    fun calls_foo(): u64 { foo() } // 合法
}

module a::n {
    fun calls_m_foo(): u64 {
        a::m::foo() // 合法,在 `a` 中也可以调用
    }
}

module b::other {
    fun calls_m_foo(): u64 {
        b::m::foo() // 错误!
//      ^^^^^^^^^^^ 'foo' 只能从 `a` 包中的模块调用
    }
}

已废弃的 public(friend) 可见性

在引入 public(package) 之前,public(friend) 用于允许有限的公共访问权限,但必须由被调用模块显式列出允许的模块列表。详细信息请参阅 Friends

entry 修饰符

除了 public 函数外,可能还有一些函数希望用作执行的入口点。entry 修饰符设计用于允许模块函数发起执行,而无需将功能公开给其他模块。

虽然 entry 函数可以作为 Move 程序的起始点,但它们并不限制于此用例。

例如:

module a::m {
    entry fun foo(): u64 { 0 }
    fun calls_foo(): u64 { foo() } // 合法!
}

module a::n {
    fun calls_m_foo(): u64 {
        a::m::foo() // 错误!
//      ^^^^^^^^^^^ 'foo' 是 'a::m' 内部的
    }
}

entry 函数可能对其参数和返回类型有限制。但这些限制是针对每个 Move 部署的特定情况的。

关于 Sui 中 entry 函数的文档可以在 这里找到

为了更轻松地进行测试,entry 函数可以从 #[test]#[test_only] 上下文中调用。

module a::m {
    entry fun foo(): u64 { 0 }
}
module a::m_test {
    #[test]
    fun my_test(): u64 { a::m::foo() } // 合法!
    #[test_only]
    fun my_test_helper(): u64 { a::m::foo() } // 合法!
}

名称

函数名称可以以字母 az 开头。在第一个字符之后,函数名称可以包含下划线 _,字母 az,字母 AZ,或数字 09

fun fOO() {}
fun bar_42() {}
fun bAZ_19() {}

类型参数

在名称之后,函数可以有类型参数

fun id<T>(x: T): T { x }
fun example<T1: copy, T2>(x: T1, y: T2): (T1, T1, T2) { (copy x, x, y) }

更多详情请参阅 Move generics

参数

函数参数通过本地变量名后跟类型注解声明

fun add(x: u64, y: u64): u64 { x + y }

我们将其读作 x 具有类型 u64

一个函数也可以完全没有任何参数。

fun useless() { }

这对于创建新的或空的数据结构非常常见。

module a::example {
  public struct Counter { count: u64 }

  fun new_counter(): Counter {
      Counter { count: 0 }
  }
}

返回类型

在参数之后,函数指定其返回类型。

fun zero(): u64 { 0 }

这里的 : u64 表示函数的返回类型是 u64

使用 元组,函数可以返回多个值:

fun one_two_three(): (u64, u64, u64) { (0, 1, 2) }

如果没有指定返回类型,函数具有隐式的返回类型单元 ()。以下函数是等效的:

fun just_unit(): () { () }
fun just_unit() { () }
fun just_unit() { }

正如在 元组部分 提到的,这些元组 "值" 在运行时并不存在。这意味着返回单元 () 的函数在执行期间不返回任何值。

函数体

函数体是一个表达式块。函数的返回值是序列中的最后一个值

fun example(): u64 {
    let x = 0;
    x = x + 1;
    x // 返回 'x'
}

有关返回值的更多信息,请参阅 下面的章节

有关表达式块的更多信息,请参阅 Move variables

原生函数

某些函数没有指定函数体,而是由虚拟机(VM)提供函数体。这些函数被标记为native

如果不修改VM源代码,程序员无法添加新的原生函数。此外,native函数的目的是用于标准库代码或特定Move环境所需的功能。

你可能会看到的大多数native函数都在标准库代码中,例如vector:

module std::vector {
    native public fun length<Element>(v: &vector<Element>): u64;
    ...
}

函数调用

调用函数时,可以通过别名或完全限定名来指定函数名:

module a::example {
    public fun zero(): u64 { 0 }
}

module b::other {
    use a::example::{Self, zero};
    fun call_zero() {
        // 有了上面的`use`语句,以下所有调用都是等价的
        a::example::zero();
        example::zero();
        zero();
    }
}

调用函数时,必须为每个参数提供一个实参。

module a::example {
    public fun takes_none(): u64 { 0 }
    public fun takes_one(x: u64): u64 { x }
    public fun takes_two(x: u64, y: u64): u64 { x + y }
    public fun takes_three(x: u64, y: u64, z: u64): u64 { x + y + z }
}

module b::other {
    fun call_all() {
        a::example::takes_none();
        a::example::takes_one(0);
        a::example::takes_two(0, 1);
        a::example::takes_three(0, 1, 2);
    }
}

类型参数可以被显式指定或推断。以下两个调用是等价的:

module aexample {
    public fun id<T>(x: T): T { x }
}

module b::other {
    fun call_all() {
        a::example::id(0);
        a::example::id<u64>(0);
    }
}

更多详情,请参见Move泛型

返回值

函数的结果,即"返回值",是其函数体的最终值。例如:

fun add(x: u64, y: u64): u64 {
    x + y
}

这里的返回值是x + y的结果。

如上所述,函数的主体是一个表达式块。表达式块可以按顺序执行各种语句,块中的最后一个表达式将成为该块的值:

fun double_and_add(x: u64, y: u64): u64 {
    let double_x = x * 2;
    let double_y = y * 2;
    double_x + double_y
}

这里的返回值是double_x + double_y的结果。

return表达式

函数隐式地返回其主体计算的值。但是,函数也可以使用显式的return表达式:

fun f1(): u64 { return 0 }
fun f2(): u64 { 0 }

这两个函数是等价的。在这个稍微复杂一点的例子中,函数将两个u64值相减,但如果第二个值太大,则提前返回0:

fun safe_sub(x: u64, y: u64): u64 {
    if (y > x) return 0;
    x - y
}

注意,这个函数的主体也可以写成if (y > x) 0 else x - y

然而,return的真正优势在于可以在其他控制流结构的深处退出。在这个例子中,函数遍历一个向量以查找给定值的索引:

use std::vector;
use std::option::{Self, Option};
fun index_of<T>(v: &vector<T>, target: &T): Option<u64> {
    let i = 0;
    let n = vector::length(v);
    while (i < n) {
        if (vector::borrow(v, i) == target) return option::some(i);
        i = i + 1
    };

    option::none()
}

不带参数使用returnreturn ()的简写。也就是说,以下两个函数是等价的:

fun foo() { return }
fun foo() { return () }

Structs and Resources

一个 结构体 是一个用户定义的数据结构,包含有类型的字段。结构体可以存储任何非引用、非元组类型,包括其他结构体。

结构体可用于定义所有“资产”值或无限制值,其操作可以由结构体的 能力 控制。默认情况下,结构体是线性的和短暂的。这意味着它们不能被复制、不能被丢弃,也不能被存储在存储中。这意味着所有值必须通过所有权转移(线性)来处理,并且在程序执行结束时必须处理这些值(短暂)。我们可以通过赋予结构体 能力 来放宽这种行为,允许值被复制或丢弃,并且可以存储在存储中或定义存储模式。

定义结构体

结构体必须在模块内定义,结构体的字段可以是命名的或位置的:

module a::m {
    public struct Foo { x: u64, y: bool }
    public struct Bar {}
    public struct Baz { foo: Foo, }
    //                          ^ 注意:在结尾处加逗号是允许的

    public struct PosFoo(u64, bool)
    public struct PosBar()
    public struct PosBaz(Foo)
}

结构体不能是递归的,因此以下定义是无效的:

public struct Foo { x: Foo }
//                     ^ 错误!递归定义

public struct A { b: B }
public struct B { a: A }
//                   ^ 错误!递归定义

public struct D(D)
//              ^ 错误!递归定义

可见性

正如你可能注意到的,所有结构体都声明为 public。这意味着结构体的类型可以从任何其他模块引用。然而,结构体的字段,以及创建或销毁结构体的能力,仍然是在定义结构体的模块内部。

在未来,我们计划添加将结构体声明为 public(package) 或作为内部的功能,类似于 函数

能力

如上所述,默认情况下,结构体声明为线性和短暂的。因此,为了允许值在这些方式下使用(例如,复制、丢弃、存储在 对象 中,或用于定义可存储的 对象),可以通过注释使用 has <ability> 来赋予结构体 能力

module a::m {
    public struct Foo has copy, drop { x: u64, y: bool }
}

能力声明可以出现在结构体字段之前或之后。然而,只能使用其中一个,不能同时使用两者。如果在结构体字段之后声明能力,则能力声明必须以分号结尾:

module a::m {
    public struct PreNamedAbilities has copy, drop { x: u64, y: bool }
    public struct PostNamedAbilities { x: u64, y: bool } has copy, drop;
    public struct PostNamedAbilitiesInvalid { x: u64, y: bool } has copy, drop
    //                                                                        ^ 错误!缺少分号

    public struct NamedInvalidAbilities has copy { x: u64, y: bool } has drop;
    //                                                               ^ 错误!重复的能力声明

    public struct PrePositionalAbilities has copy, drop (u64, bool)
    public struct PostPositionalAbilities (u64, bool) has copy, drop;
    public struct PostPositionalAbilitiesInvalid (u64, bool) has copy, drop
    //                                                                     ^ 错误!缺少分号
    public struct InvalidAbilities has copy (u64, bool) has drop;
    //                                                  ^ 错误!重复的能力声明
}

更多细节,请参阅关于 注释结构体和枚举的能力 的部分。

命名

结构体的名称必须以大写字母 AZ 开头。在第一个字母之后,结构体名称可以包含下划线 _、字母 az、字母 AZ 或数字 09

public struct Foo {}
public struct BAR {}
public struct B_a_z_4_2 {}
public struct P_o_s_Foo()

这种以 AZ 开头的命名限制是为了为未来的语言功能留出空间。它可能会被移除,也可能会在以后保留。

使用结构体

创建结构体

可以通过指定结构体名称,后跟每个字段的值来创建(或“打包”)结构体类型的值。

对于具有命名字段的结构体,字段的顺序不重要,但必须提供字段名称。对于具有位置字段的结构体,字段的顺序必须与结构体定义中字段的顺序相匹配,并且必须使用 () 而不是 {} 来括起参数。

module a::m {
    public struct Foo has drop { x: u64, y: bool }
    public struct Baz has drop { foo: Foo }
    public struct Positional(u64, bool) has drop;

    fun example() {
        let foo = Foo { x: 0, y: false };
        let baz = Baz { foo: foo };
        // 注意:位置结构体值是使用括号创建的,基于位置而不是名称。
        let pos = Positional(0, false);
        let pos_invalid = Positional(false, 0);
        //                           ^ 错误!字段顺序不正确且类型不匹配。
    }
}

对于具有命名字段的结构体,如果有与字段名称相同的局部变量,可以使用以下简写:

let baz = Baz { foo: foo };
// 等同于
let baz = Baz { foo };

这有时被称为“字段名捕捉”。

通过模式匹配销毁结构体

可以通过将结构体值绑定或分配到模式中来销毁结构体值,使用的语法与构造它们的语法类似。

module a::m {
    public struct Foo { x: u64, y: bool }
    public struct Bar(Foo)
    public struct Baz {}
    public struct Qux()

    fun example_destroy_foo() {
        let foo = Foo { x: 3, y: false };
        let Foo { x, y: foo_y } = foo;
        //        ^ shorthand for `x: x`

        // two new bindings
        //   x: u64 = 3
        //   foo_y: bool = false
    }

    fun example_destroy_foo_wildcard() {
        let foo = Foo { x: 3, y: false };
        let Foo { x, y: _ } = foo;

        // only one new binding since y was bound to a wildcard
        //   x: u64 = 3
    }

    fun example_destroy_foo_assignment() {
        let x: u64;
        let y: bool;
        Foo { x, y } = Foo { x: 3, y: false };

        // mutating existing variables x and y
        //   x = 3, y = false
    }

    fun example_foo_ref() {
        let foo = Foo { x: 3, y: false };
        let Foo { x, y } = &foo;

        // two new bindings
        //   x: &u64
        //   y: &bool
    }

    fun example_foo_ref_mut() {
        let foo = Foo { x: 3, y: false };
        let Foo { x, y } = &mut foo;

        // two new bindings
        //   x: &mut u64
        //   y: &mut bool
    }

    fun example_destroy_bar() {
        let bar = Bar(Foo { x: 3, y: false });
        let Bar(Foo { x, y }) = bar;
        //            ^ nested pattern

        // two new bindings
        //   x: u64 = 3
        //   y: bool = false
    }

    fun example_destroy_baz() {
        let baz = Baz {};
        let Baz {} = baz;
    }

    fun example_destroy_qux() {
        let qux = Qux();
        let Qux() = qux;
    }
}

访问结构体字段

结构体的字段可以使用点操作符 . 进行访问。

对于具有命名字段的结构体,可以通过字段名称进行访问:

public struct Foo { x: u64, y: bool }
let foo = Foo { x: 3, y: true };
let x = foo.x;  // x == 3
let y = foo.y;  // y == true

对于位置结构体,可以通过它们在结构体定义中的位置进行访问:

public struct PosFoo(u64, bool)
let pos_foo = PosFoo(3, true);
let x = pos_foo.0;  // x == 3
let y = pos_foo.1;  // y == true

在不借用或复制结构体字段的情况下访问它们受字段能力约束的限制。更多详情请参阅 借用结构体和字段读取和写入字段 部分。

借用结构体和字段

可以使用 &&mut 操作符创建对结构体或字段的引用。这些例子包含了一些可选的类型注释(例如,: &Foo)来展示操作的类型。

let foo = Foo { x: 3, y: true };
let foo_ref: &Foo = &foo;
let y: bool = foo_ref.y;         // 通过引用读取结构体的字段
let x_ref: &u64 = &foo.x;        // 通过扩展对结构体的引用借用字段

let x_ref_mut: &mut u64 = &mut foo.x;
*x_ref_mut = 42;            // 通过可变引用修改字段

可以借用嵌套结构体的内部字段:

let foo = Foo { x: 3, y: true };
let bar = Bar(foo);

let x_ref = &bar.0.x;

你也可以通过对结构体的引用借用字段:

let foo = Foo { x: 3, y: true };
let foo_ref = &foo;
let x_ref = &foo_ref.x;
// 这与 let x_ref = &foo.x 的效果相同

读取和写入字段

如果需要读取并复制字段的值,可以解引用借用的字段:

let foo = Foo { x: 3, y: true };
let bar = Bar(copy foo);
let x: u64 = *&foo.x;
let y: bool = *&foo.y;
let foo2: Foo = *&bar.0;

使用点操作符 . 可以读取结构体的字段,而不需要借用。与解引用一样,字段类型必须具有 copy 能力

let foo = Foo { x: 3, y: true };
let x = foo.x;  // x == 3
let y = foo.y;  // y == true

点操作符可以链式调用以访问嵌套字段:

let bar = Bar(Foo { x: 3, y: true });
let x = bar.0.x; // x == 3;

但是,这不允许包含非原始类型字段的结构体,如向量或其他结构体:

let foo = Foo { x: 3, y: true };
let bar = Bar(foo);
let foo2: Foo = *&bar.0;
let foo3: Foo = bar.0; // 错误! 必须添加显式复制 *&

我们可以借用结构体的字段以赋予它新值:

let mut foo = Foo { x: 3, y: true };
*&mut foo.x = 42;     // foo = Foo { x: 42, y: true }
*&mut foo.y = !foo.y; // foo = Foo { x: 42, y: false }
let mut bar = Bar(foo);               // bar = Bar(Foo { x: 42, y: false })
*&mut bar.0.x = 52;                   // bar = Bar(Foo { x: 52, y: false })
*&mut bar.0 = Foo { x: 62, y: true }; // bar = Bar(Foo { x: 62, y: true })

与解引用类似,我们可以直接使用点操作符来修改字段。在这两种情况下,字段类型必须具有 drop 能力

let mut foo = Foo { x: 3, y: true };
foo.x = 42;     // foo = Foo { x: 42, y: true }
foo.y = !foo.y; // foo = Foo { x: 42, y: false }
let mut bar = Bar(foo);         // bar = Bar(Foo { x: 42, y: false })
bar.0.x = 52;                   // bar = Bar(Foo { x: 52, y: false })
bar.0 = Foo { x: 62, y: true }; // bar = Bar(Foo { x: 62, y: true })

点语法用于通过结构体的引用进行赋值也适用:

let foo = Foo { x: 3, y: true };
let foo_ref = &mut foo;
foo_ref.x = foo_ref.x + 1;

特权结构体操作

大多数针对结构体类型 T 的结构体操作只能在声明 T 的模块内部执行:

  • 结构体类型只能在定义结构体的模块内部创建("packed")、销毁("unpacked")。
  • 结构体的字段只能在定义结构体的模块内部访问。

遵循这些规则,如果你想在模块外修改结构体,需要为其提供公共 API。本章结尾包含一些示例。

然而,如上面的可见性部分所述,结构体 类型 对其他模块始终可见。

module a::m {
    public struct Foo has drop { x: u64 }

    public fun new_foo(): Foo {
        Foo { x: 42 }
    }
}

module a::n {
    use a::m::Foo;

    public struct Wrapper has drop {
        foo: Foo
        //   ^ 类型是公共的,因此有效
    }

    fun f1(foo: Foo) {
        let x = foo.x;
        //      ^ 错误! 无法在 `a::m` 外部访问 `Foo` 的字段
    }

    fun f2() {
        let foo_wrapper = Wrapper { foo: m::new_foo() };
        //                               ^ 函数是公共的,因此有效
    }
}

所有权

如上文定义结构体中提到的,默认情况下,结构体是线性和短暂的。这意味着它们不能被复制或丢弃。这一特性在模拟现实世界资产(如货币)时非常有用,因为你不希望货币被复制或在流通中丢失。

module a::m {
    public struct Foo { x: u64 }

    public fun copying() {
        let foo = Foo { x: 100 };
        let foo_copy = copy foo; // 错误! 复制需要 'copy' 能力
        let foo_ref = &foo;
        let another_copy = *foo_ref // 错误! 解引用需要 'copy' 能力
    }

    public fun destroying_1() {
        let foo = Foo { x: 100 };

        // 错误! 当函数返回时,foo 仍包含值。
        // 这种销毁需要 'drop' 能力
    }

    public fun destroying_2(f: &mut Foo) {
        *f = Foo { x: 100 } // 错误!
                            // 通过写入销毁旧值需要 'drop' 能力
    }
}

要修复 fun destroying_1 示例,需要手动“解包”值:

module a::m {
    public struct Foo { x: u64 }

    public fun destroying_1_fixed() {
        let foo = Foo { x: 100 };
        let Foo { x: _ } = foo;
    }
}

请记住,只有在定义结构体的模块中才能解构结构体。这可以用来在系统中强制执行某些不变量,例如货币的保存。

另一方面,如果你的结构体不代表有价值的东西,可以添加 copydrop 能力,以获得更符合其他编程语言习惯的结构体值:

module a::m {
    public struct Foo has copy, drop { x: u64 }

    public fun run() {
        let foo = Foo { x: 100 };
        let foo_copy = foo;
        //             ^ 这段代码复制了 foo,
        //             而 `let x = move foo` 会移动 foo

        let x = foo.x;            // x = 100
        let x_copy = foo_copy.x;  // x = 100

        // 当函数返回时,foo 和 foo_copy 都被隐式丢弃
    }
}

存储

结构体可以用来定义存储模式,但具体细节因 Move 的不同部署而异。有关更多详情,请参阅 key 能力Sui 对象 的文档。

Enumerations

枚举(enum)是一种用户定义的数据结构,包含一个或多个变体(variant)。每个变体可以选择性地包含带类型的字段。这些字段的数量和类型可以在枚举的各个变体之间不同。枚举中的字段可以存储任何非引用、非元组类型,包括其他结构体或枚举。

下面是 Move 中的一个简单枚举定义示例:

public enum Action {
    Stop,
    Pause { duration: u32 },
    MoveTo { x: u64, y: u64 },
    Jump(u64),
}

这定义了一个名为Action的枚举,表示游戏中可以采取的不同动作 —— 你可以Stop(停止),Pause(暂停)一段时间,MoveTo(移动到)特定位置,或者Jump(跳跃)到特定高度。

与结构体类似,枚举也可以拥有能力,这些能力控制可以对枚举执行哪些操作。需要注意的是,枚举不能拥有key能力,因为它们不能作为顶级对象。

定义枚举

枚举必须在模块中定义,一个枚举必须至少包含一个变体,每个变体可以没有字段、有位置字段或命名字段。以下是一些示例:

module a::m {
    public enum Foo has drop {
        VariantWithNoFields,
        //                 ^ 注意:变体声明后可以有一个尾随逗号
    }
    public enum Bar has copy, drop {
        VariantWithPositionalFields(u64, bool),
    }
    public enum Baz has drop {
        VariantWithNamedFields { x: u64, y: bool, z: Bar },
    }
}

枚举在任何变体中都不能递归,所以以下枚举定义是不允许的,因为它们至少在一个变体中是递归的。

错误示例:

module a::m {
    public enum Foo {
        Recursive(Foo),
        //        ^ 错误:递归枚举变体
    }
    public enum List {
        Nil,
        Cons { head: u64, tail: List },
        //                      ^ 错误:递归枚举变体
    }
    public enum BTree<T> {
        Leaf(T),
        Node { left: BTree<T>, right: BTree<T> },
        //           ^ 错误:递归枚举变体
    }

    // 相互递归的枚举也是不允许的
    public enum MutuallyRecursiveA {
        Base,
        Other(MutuallyRecursiveB),
        //    ^^^^^^^^^^^^^^^^^^ 错误:递归枚举变体
    }

    public enum MutuallyRecursiveB {
        Base,
        Other(MutuallyRecursiveA),
        //    ^^^^^^^^^^^^^^^^^^ 错误:递归枚举变体
    }
}

可见性

所有枚举都被声明为public。这意味着枚举的类型可以从任何其他模块引用。但是,枚举的变体、每个变体中的字段以及创建或销毁枚举变体的能力仅限于定义该枚举的模块内部。

能力

就像结构体一样,默认情况下枚举声明是线性和短暂的。要以非线性或非短暂的方式使用枚举值 —— 即复制、丢弃或存储在对象中 —— 你需要通过用has <ability>注释来授予它额外的能力:

module a::m {
    public enum Foo has copy, drop {
        VariantWithNoFields,
    }
}

能力声明可以在枚举变体之前或之后出现,但只能使用其中一种方式,不能两种都用。如果在变体之后声明,能力声明必须以分号结束:

module a::m {
    public enum PreNamedAbilities has copy, drop { Variant }
    public enum PostNamedAbilities { Variant } has copy, drop;
    public enum PostNamedAbilitiesInvalid { Variant } has copy, drop
    //                                                              ^ 错误! 缺少分号

    public enum NamedInvalidAbilities has copy { Variant } has drop;
    //                                                     ^ 错误! 重复的能力声明
}

更多详情,请参阅注释能力部分。

命名

枚举和枚举内的变体必须以大写字母AZ开头。在第一个字母之后,枚举名可以包含下划线_、小写字母az、大写字母AZ或数字09

public enum Foo { Variant }
public enum BAR { Variant }
public enum B_a_z_4_2 { V_a_riant_0 }

这种以AZ开头的命名限制是为了给未来的语言特性留出空间。

使用枚举

创建枚举变体

可以通过指定枚举的一个变体,然后为该变体中的每个字段提供一个值来创建(或"打包")枚举类型的值。变体名称必须始终由枚举名称限定。

与结构体类似,对于具有命名字段的变体,字段的顺序并不重要,但需要提供字段名称。对于具有位置字段的变体,字段的顺序很重要,必须与变体声明中的顺序匹配。它还必须使用()而不是{}来创建。如果变体没有字段,变体名称就足够了,不需要使用(){}

module a::m {
    public enum Action has drop {
        Stop,
        Pause { duration: u32 },
        MoveTo { x: u64, y: u64 },
        Jump(u64),
    }
    public enum Other has drop {
        Stop(u64),
    }

    fun example() {
        // 注意: `Action`的`Stop`变体没有字段,所以不需要括号或大括号。
        let stop = Action::Stop;
        let pause = Action::Pause { duration: 10 };
        let move_to = Action::MoveTo { x: 10, y: 20 };
        let jump = Action::Jump(10);
        // 注意: `Other`的`Stop`变体确实有位置字段,所以我们需要提供它们。
        let other_stop = Other::Stop(10);
    }
}

对于具有命名字段的变体,你也可以使用你可能从结构体中熟悉的简写语法来创建变体:

let duration = 10;

let pause = Action::Pause { duration: duration };
// 等同于
let pause = Action::Pause { duration };

枚举变体和解构的模式匹配

由于枚举值可以采用不同的形式,因此不允许像结构字段那样直接访问变体的字段。相反,要访问变体内部的字段(无论是通过值、不可变引用还是可变引用),您必须使用模式匹配。

在Move中,可以通过值、不可变引用和可变引用进行模式匹配。通过值进行模式匹配时,该值被移动到匹配的分支中。通过引用进行模式匹配时,该值被借用到匹配的分支中(可以是不可变的或可变的)。我们在这里简要介绍使用 match 进行模式匹配,但如果想了解更多关于Move中使用 match 进行模式匹配的信息,请参阅模式匹配部分。

match语句用于对Move值进行模式匹配,由多个匹配分支组成。每个匹配分支由一个模式、一个箭头 => 和一个表达式组成,后跟逗号 ,。模式可以是结构体、枚举变体、绑定(xy)、通配符(_..)、常量(ConstValue)或文字值(true42等)。值将从上到下依次与每个模式进行匹配,并匹配第一个结构上匹配的模式。一旦值匹配成功,就会执行 => 右侧的表达式。

此外,匹配分支可以有可选的 条件,在模式匹配成功后但在执行表达式之前进行检查。条件由 if 关键字指定,后跟必须评估为布尔值的表达式。

下面是一个使用枚举和模式匹配的示例,展示了如何根据不同的枚举变体执行不同的操作:

module a::m {
    public enum Action has drop {
        Stop,
        Pause { duration: u32 },
        MoveTo { x: u64, y: u64 },
        Jump(u64),
    }

    public struct GameState {
        // 游戏状态包含的字段
        character_x: u64,
        character_y: u64,
        character_height: u64,
        // ...
    }

    fun perform_action(state: &mut GameState, action: Action) {
        match action {
            // 处理 `Stop` 变体
            Action::Stop => state.stop(),
            // 处理 `Pause` 变体
            // 如果持续时间为 0,则什么也不做
            Action::Pause { duration: 0 } => (),
            Action::Pause { duration } => state.pause(duration),
            // 处理 `MoveTo` 变体
            Action::MoveTo { x, y } => state.move_to(x, y),
            // 处理 `Jump` 变体
            // 如果游戏不允许跳跃,则什么也不做
            Action::Jump(_) if state.jumps_not_allowed() => (),
            Action::Jump(height) => state.jump(height),
        }
    }
}

接下来,我们看看如何在枚举上进行模式匹配,以在可变情况下更新其值。我们将以一个简单的枚举为例,其中有两个变体,每个变体都有一个字段。然后编写两个函数,一个仅递增第一个变体的值,另一个仅递增第二个变体的值:

module a::m {
    public enum SimpleEnum {
        Variant1(u64),
        Variant2(u64),
    }

    public fun incr_enum_variant1(simple_enum: &mut SimpleEnum) {
        match simple_enum {
            SimpleEnum::Variant1(value) => *value += 1,
            _ => (),
        }
    }

    public fun incr_enum_variant2(simple_enum: &mut SimpleEnum) {
        match simple_enum {
            SimpleEnum::Variant2(value) => *value += 1,
            _ => (),
        }
    }
}

现在,如果有一个 SimpleEnum 的值,我们可以使用这些函数来递增该变体的值:

let mut x = SimpleEnum::Variant1(10);
incr_enum_variant1(&mut x);
assert!(x == SimpleEnum::Variant1(11));
// 由于它递增了不同的变体,因此不会递增
incr_enum_variant2(&mut x);
assert!(x == SimpleEnum::Variant1(11));

当在Move值上进行模式匹配时,如果值没有 drop 能力,则必须在每个匹配分支中消耗或解构该值。如果在匹配分支中未消耗或解构值,则编译器会引发错误。这是为了确保在匹配语句中处理了所有可能的值。

例如,考虑以下代码:

module a::m {
    public enum X { Variant { x: u64 } }

    public fun bad(x: X) {
        match x {
            _ => ()
           // ^ 错误!在此匹配分支中未消耗或解构类型为 `X` 的值
        }
    }
}

要正确处理这种情况,您需要在匹配的分支中解构 X 及其所有变体:

module a::m {
    public enum X { Variant { x: u64 } }

    public fun good(x: X) {
        match x {
            // OK!编译通过,因为值已解构
            X::Variant { x: _ } => ()
        }
    }
}

覆盖枚举值

只要枚举具有 drop 能力,您就可以像在Move中处理其他值一样,使用新类型的枚举值覆盖枚举的值。

module a::m {
    public enum X has drop {
        A(u64),
        B(u64),
    }

    public fun overwrite_enum(x: &mut X) {
        *x = X::A(10);
    }
}
let mut x = X::B(20);
overwrite_enum(&mut x);
assert!(x == X::A(10));

Constants

常量是在模块内为共享的、静态的值命名的一种方式。

常量的值必须在编译时就能确定。常量的值会存储在编译后的模块中。每次使用常量时,都会创建该值的一个新副本。

声明

常量声明以const关键字开始,后面跟着名称、类型和值。

const <名称>: <类型> = <表达式>;

例如:

module a::example {
    const MY_ADDRESS: address = @a;

    public fun permissioned(addr: address) {
        assert!(addr == MY_ADDRESS, 0);
    }
}

命名规则

常量必须以大写字母AZ开头。首字母之后,常量名可以包含下划线_、小写字母az、大写字母AZ或数字09

const FLAG: bool = false;
const EMyErrorCode: u64 = 0;
const ADDRESS_42: address = @0x42;

尽管你可以在常量中使用小写字母az,但通用样式指南建议只使用大写字母AZ,并在每个单词之间使用下划线_。对于错误代码,我们使用E作为前缀,然后对其余部分使用大驼峰命名法(也称为帕斯卡命名法),如EMyErrorCode所示。

当前要求以AZ开头的命名限制是为了给未来的语言特性留出空间。

可见性

目前不支持publicpublic(package)常量。const值只能在声明它的模块中使用。不过,为了方便起见,它们可以在单元测试属性中跨模块使用。

有效表达式

目前,常量限于原始类型boolu8u16u32u64u128u256addressvector<T>,其中T是常量的有效类型。

通常,const被赋予一个简单的值或其类型的字面量。例如:

const MY_BOOL: bool = false;
const MY_ADDRESS: address = @0x70DD;
const BYTES: vector<u8> = b"hello world";
const HEX_BYTES: vector<u8> = x"DEADBEEF";

复杂表达式

除了字面量,常量还可以包含更复杂的表达式,只要编译器能够在编译时将表达式简化为一个值即可。

目前,可以使用相等操作、所有布尔操作、所有位操作和所有算术操作。

const RULE: bool = true && false;
const CAP: u64 = 10 * 100 + 1;
const SHIFTY: u8 = {
    (1 << 1) * (1 << 2) * (1 << 3) * (1 << 4)
};
const HALF_MAX: u128 = 340282366920938463463374607431768211455 / 2;
const REM: u256 =
    57896044618658097711785492504343953926634992332820282019728792003956564819968 % 654321;
const EQUAL: bool = 1 == 1;

如果操作会导致运行时异常,编译器会给出无法生成常量值的错误:

const DIV_BY_ZERO: u64 = 1 / 0; // 错误!
const SHIFT_BY_A_LOT: u64 = 1 << 100; // 错误!
const NEGATIVE_U64: u64 = 0 - 1; // 错误!

此外,常量可以引用同一模块内的其他常量:

const BASE: u8 = 4;
const SQUARE: u8 = BASE * BASE;

但请注意,常量定义中的任何循环引用都会导致错误:

const A: u16 = B + 1;
const B: u16 = A + 1; // 错误!

Generics(泛型)

泛型可以用来定义函数和结构体,使它们可以适用于不同的输入数据类型。在 Move 中,我们通常将这种语言特性称为参数化多态性。泛型参数和类型参数的术语在 Move 中可以互换使用。

泛型通常用于库代码中,例如在 vector 中,用于声明可以处理任何可能类型(满足指定约束条件)的代码。这种参数化允许您在多种类型和情况下重用相同的实现。

声明类型参数

函数和结构体可以在它们的签名中使用一组类型参数列表,用尖括号 <...> 括起来。

泛型函数

函数的类型参数位于函数名之后和(值)参数列表之前。以下代码定义了一个泛型的身份函数,它接受任何类型的值并返回该值本身。

fun id<T>(x: T): T {
    // 这种类型注解是不必要的但是有效的
    (x: T)
}

一旦定义,类型参数 T 可以在参数类型、返回类型和函数体内部使用。

泛型结构体

结构体的类型参数位于结构体名字之后,可以用来命名字段的类型。

public struct Foo<T> has copy, drop { x: T }

public struct Bar<T1, T2> has copy, drop {
    x: T1,
    y: vector<T2>,
}

请注意,类型参数不一定要被使用

类型参数

调用泛型函数

在调用泛型函数时,可以在尖括号内指定函数的类型参数。

fun foo() {
    let x = id<bool>(true);
}

如果您没有指定类型参数,Move 的 类型推断 将为您提供它们。

使用泛型结构体

类似地,可以在构造或销毁泛型类型的值时附加一个类型参数列表。

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 的 类型推断 将为您提供它们。

类型参数不匹配

如果指定的类型参数与实际提供的值冲突,则会产生错误:

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 不兼容
}

类型推断

在大多数情况下,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> 被推断出来了
    }
}

整数

在 Move 中,整数类型 u8u16u32u64u128u256 都是不同的类型。然而,每种类型都可以用相同的数值语法创建。换句话说,如果没有提供类型后缀,编译器将根据值的使用情况推断整数类型。

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;
//                                             ^^^^ 有效!

未使用的类型参数

对于结构体定义,未使用的类型参数是指在结构体定义的任何字段中都没有出现的类型参数,但在编译时会进行静态检查。Move 允许未使用的类型参数,因此以下结构体定义是有效的:

public struct Foo<T> {
    foo: u64
}

在建模某些概念时,这样做非常方便。以下是一个示例:

module a::m {
    // 货币说明符
    public struct A {}
    public struct B {}

    // 一个通用的硬币类型,可以使用货币说明符类型进行实例化。
    //   例如 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 类型参数,这种通用性也适用。

Phantom Type Parameters

In the example above, although struct Coin asks for the store ability, neither Coin<A> nor Coin<B> will have the store ability. This is because of the rules for Conditional Abilities and Generic Types and the fact that A and B don't have the store ability, despite the fact that they are not even used in the body of struct Coin. This might cause some unpleasant consequences. For example, we are unable to put Coin<A> into a wallet in storage.

One possible solution would be to add spurious ability annotations to A and B (i.e., public struct Currency1 has store {}). But, this might lead to bugs or security vulnerabilities because it weakens the types with unnecessary ability declarations. For example, we would never expect a value in the storage to have a field in type A, but this would be possible with the spurious store ability. Moreover, the spurious annotations would be infectious, requiring many functions generic on the unused type parameter to also include the necessary constraints.

Phantom type parameters solve this problem. Unused type parameters can be marked as phantom type parameters, which do not participate in the ability derivation for structs. In this way, arguments to phantom type parameters are not considered when deriving the abilities for generic types, thus avoiding the need for spurious ability annotations. For this relaxed rule to be sound, Move's type system guarantees that a parameter declared as phantom is either not used at all in the struct definition, or it is only used as an argument to type parameters also declared as phantom.

Declaration

In a struct definition a type parameter can be declared as phantom by adding the phantom keyword before its declaration.

public struct Coin<phantom Currency> has store {
    value: u64
}

If a type parameter is declared as phantom we say it is a phantom type parameter. When defining a struct, Move's type checker ensures that every phantom type parameter is either not used inside the struct definition or it is only used as an argument to a phantom type parameter.

public struct S1<phantom T1, T2> { f: u64 }
//               ^^^^^^^ valid, T1 does not appear inside the struct definition

public struct S2<phantom T1, T2> { f: S1<T1, T2> }
//               ^^^^^^^ valid, T1 appears in phantom position

The following code shows examples of violations of the rule:

public struct S1<phantom T> { f: T }
//               ^^^^^^^ ERROR!  ^ Not a phantom position

public struct S2<T> { f: T }
public struct S3<phantom T> { f: S2<T> }
//               ^^^^^^^ ERROR!     ^ Not a phantom position

More formally, if a type is used as an argument to a phantom type parameter we say the type appears in phantom position. With this definition in place, the rule for the correct use of phantom parameters can be specified as follows: A phantom type parameter can only appear in phantom position.

Note that specifying phantom is not required, but the compiler will warn if a type parameter could be phantom but was not marked as such.

Instantiation

When instantiating a struct, the arguments to phantom parameters are excluded when deriving the struct abilities. For example, consider the following code:

public struct S<T1, phantom T2> has copy { f: T1 }
public struct NoCopy {}
public struct HasCopy has copy {}

Consider now the type S<HasCopy, NoCopy>. Since S is defined with copy and all non-phantom arguments have copy then S<HasCopy, NoCopy> also has copy.

拥有 Ability 约束的 Phantom 类型参数

在之前的例子中,尽管struct Coin要求有store能力,但Coin<A>Coin<B>都不会有store能力。这是因为条件能力和泛型类型的规则,以及AB没有store能力,尽管它们在struct Coin的主体中甚至没有被使用。这可能会导致一些不愉快的后果。例如,我们无法将Coin<A>放入存储中的钱包。

一个可能的解决方案是为AB添加虚假的能力注释(例如,public struct Currency1 has store {})。但是,这可能会导致错误或安全漏洞,因为它用不必要的能力声明削弱了类型。例如,我们永远不会期望存储中的值有一个A类型的字段,但有了虚假的store能力,这就成为可能。此外,这些虚假注释会具有传染性,要求许多使用未使用类型参数的泛型函数也包含必要的约束。

幻象类型参数解决了这个问题。未使用的类型参数可以被标记为幻象类型参数,它们不参与结构体的能力推导。这样,幻象类型参数的参数在推导泛型类型的能力时不会被考虑,从而避免了虚假能力注释的需要。为了使这个放宽的规则是安全的,Move的类型系统保证了声明为phantom的参数要么在结构体定义中完全不使用,要么只作为参数用于同样声明为phantom的类型参数。

声明

在结构体定义中,可以通过在类型参数声明前添加phantom关键字来将其声明为幻象类型参数。

public struct Coin<phantom Currency> has store {
    value: u64
}

如果一个类型参数被声明为幻象,我们称之为幻象类型参数。在定义结构体时,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但未被标记为phantom,编译器会发出警告。

实例化

在实例化结构体时,幻象参数的参数在推导结构体能力时被排除。例如,考虑以下代码:

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能力。

带能力约束的幻象类型参数

能力约束和幻象类型参数是正交的特性,意味着幻象参数可以声明为带有能力约束。

public struct S<phantom T: copy> {}

在实例化带有能力约束的幻象类型参数时,类型参数必须满足该约束,尽管该参数是幻象的。通常的限制适用,T只能用具有copy能力的参数实例化。

约束

在上面的例子中,我们演示了如何使用类型参数来定义可以由调用者在稍后填充的"未知"类型。然而,这意味着类型系统对该类型的信息很少,必须以非常保守的方式进行检查。在某种意义上,类型系统必须假设无约束泛型的最坏情况 —— 一个没有能力的类型。

约束提供了一种方法来指定这些未知类型具有哪些属性,以便类型系统可以允许原本不安全的操作。

声明约束

可以使用以下语法对类型参数施加约束:

// T 是类型参数的名称
T: <ability> (+ <ability>)*

<ability>可以是四种能力中的任何一种,一个类型参数可以同时被多个能力约束。因此,以下都是有效的类型参数声明:

T: copy
T: copy + drop
T: copy + drop + store + key

验证约束

约束在实例化点进行检查

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' 能力
}

更多信息,请参见能力部分的条件能力和泛型类型

递归限制

递归结构体

泛型结构体不能直接或间接地包含相同类型的字段,即使有不同的类型参数也不行。以下所有结构体定义都是无效的:

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>
}

高级主题: 类型级递归

Move允许泛型函数递归调用。然而,当与泛型结构体结合使用时,这可能在某些情况下创建无限数量的类型,允许这种情况会给编译器、虚拟机和其他语言组件增加不必要的复杂性。因此,这种递归是被禁止的。

这个限制可能在将来会放宽,但目前,以下示例应该能让你了解什么是允许的,什么是不允许的。

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 的类型系统认为是无效的。

能力

能力是Move语言中的一个类型特性,用于控制某种类型的值可以执行哪些操作。这套系统提供了对值的“线性”类型行为的细粒度控制,以及值在存储中的使用方式(由Move的具体部署定义,例如区块链中的存储概念)。这是通过限制对某些字节码指令的访问来实现的,只有当一个值具有所需的能力(如果需要的话——并非每个指令都需要能力)时,才能使用这些字节码指令。

对于Sui,key用于表示一个对象。对象是存储的基本单位,每个对象都有一个唯一的32字节ID。store则用于指示可以存储在对象内部的数据类型,同时也用于指示哪些类型可以在定义模块之外进行转移。

四种能力

四种能力分别是:

  • copy
    • 允许具有此能力的类型的值被复制。
  • drop
    • 允许具有此能力的类型的值被丢弃。
  • store
    • 允许具有此能力的类型的值存在于存储中的某个值内。
    • 对于Sui,store控制哪些数据可以存储在对象内,也控制哪些类型可以在定义模块之外转移。
  • key
    • 允许类型作为存储的“键”。这意味着该值可以作为存储中的顶级值;换句话说,它不需要包含在其他值中即可存在于存储中。
    • 对于Sui,key用于表示一个对象

copy

copy能力允许具有此能力的类型的值被复制。它限制了从局部变量中复制值的能力,通过copy操作符以及通过引用复制值的能力,通过dereference *e操作符。

如果一个值具有copy,那么该值内部包含的所有值都具有copy

drop

drop能力允许具有此能力的类型的值被丢弃。丢弃是指该值未被转移且在Move程序执行时被有效地销毁。因此,这种能力限制了在多个位置忽略值的能力,包括:

如果一个值具有drop,那么该值内部包含的所有值都具有drop

store

store能力允许具有此能力的类型的值存在于存储中的某个值内,但不一定作为存储中的顶级值。这是唯一一个不直接限制操作的能力。相反,它在与key一起使用时限制了在存储中的存在。

如果一个值具有store,那么该值内部包含的所有值都具有store

对于Sui,store有双重作用。它控制哪些值可以出现在一个对象内部,以及哪些对象可以在定义模块之外转移

key

key能力允许类型作为存储操作的键,如Move的部署所定义。虽然它特定于每个Move实例,但它用于限制所有存储操作,因此要想使用存储原语,类型必须具有key能力。

如果一个值具有key,那么该值内部包含的所有值都具有store。这是唯一具有这种不对称性的能力。

对于Sui,key用于表示一个对象

内建类型

所有原始内建类型都具有copydropstore

  • boolu8u16u32u64u128u256address都具有copydropstore
  • vector<T>可能具有copydropstore,这取决于T的能力。
  • 不可变引用&和可变引用&mut都具有copydrop
    • 这指的是复制和丢弃引用本身,而不是它们引用的内容。
    • 引用不能出现在全局存储中,因此它们不具有store

注意,原始类型中没有一个具有key,这意味着它们不能直接用于存储操作。

标注结构体和枚举

要声明一个结构体或枚举具有某种能力,可以在数据类型名称之后、字段/变体之前或之后使用has <ability>进行声明。例如:

public struct Ignorable has drop { f: u64 }
public struct Pair has copy, drop, store { x: u64, y: u64 }
public struct MyVec(vector<u64>) has copy, drop, store;

public enum IgnorableEnum has drop { Variant }
public enum PairEnum has copy, drop, store { Variant }
public enum MyVecEnum { Variant } has copy, drop, store;

在这种情况下:Ignorable*具有drop能力。Pair*MyVec*都具有copydropstore

所有这些能力对这些受限制的操作具有强保证。只有当值具有这种能力时,才能对该值执行操作;即使该值深深嵌套在某个集合中也是如此!

因此:在声明结构体的能力时,对字段有一定的要求。所有字段必须满足这些约束。这些规则是必要的,以便结构体满足上述能力的可达性规则。如果一个结构体被声明具有能力...

  • copy,所有字段必须具有copy
  • drop,所有字段必须具有drop
  • store,所有字段必须具有store
  • key,所有字段必须具有store
    • key是目前唯一不要求自身的能力。

枚举可以具有上述任何能力,除了key,因为枚举不能作为存储中的顶级值(对象)。对于枚举变体的字段,规则与结构体字段相同。如果枚举被声明具有能力...

  • copy,所有变体的所有字段必须具有copy
  • drop,所有变体的所有字段必须具有drop
  • store,所有变体的所有字段必须具有store
  • key,枚举不允许具有此能力。

例如:

// 一个没有任何能力的结构体
public struct NoAbilities {}

public struct WantsCopy has copy {
    f: NoAbilities, // 错误 'NoAbilities' 不具有 'copy'
}

public enum WantsCopyEnum has copy {
    Variant1
    Variant2(NoAbilities), // 错误 'NoAbilities' 不具有 'copy'
}

同样地:

// 一个没有任何能力的结构体
public struct NoAbilities {}

public struct MyData has key {
    f: NoAbilities, // 错误 'NoAbilities' 不具有 'store'
}

public struct MyDataEnum has store {
    Variant1,
    Variant2(NoAbilities), // 错误 'NoAbilities' 不具有 'store'
}

条件能力和泛型类型

当能力标注在泛型类型上时,并不是该类型的所有实例都保证具有该能力。考虑以下结构体声明:

// public struct Cup<T> has copy, drop, store, key { item: T }

如果Cup能够容纳任何类型,而不考虑其能力,那将非常有帮助。类型系统可以_看到_类型参数,因此如果它_看到_一个类型参数会违反该能力的保证,它应该能够从Cup中删除该能力。

这种行为一开始可能听起来有点混乱,但如果我们考虑集合类型,它可能会更容易理解。我们可以将内建类型vector看作具有以下类型声明:

vector<T> has copy, drop, store;

我们希望vector能与任何类型一起工作。我们不希望为不同的能力创建单独的vector类型。那么我们希望的规则是什么?正是我们在上述字段规则中希望的规则。因此,只有当内部元素可以复制时,才可以复制vector值。只有当内部元素可以忽略/丢弃时,才可以忽略vector值。而且,只有当内部元素可以存在于存储中时,才可以将vector放入存储中。

为了具有这种额外的表达能力,类型可能没有它声明的所有能力,具体取决于该类型的实例化;相反,一个类型将具有的能力取决于其声明其类型参数。对于任何类型,类型参数被悲观地假设用于结构体内部,因此只有当类型参数满足上述字段的要求时,才授予能力。以上面的Cup为例:

  • 只有当T具有copy时,Cup才具有copy能力。
  • 只有当`T

具有drop时,它才具有drop`能力。

  • 只有当T具有store时,它才具有store能力。
  • 只有当T具有store时,它才具有key能力。

以下是每种能力的条件系统示例:

示例:条件copy

public struct NoAbilities {}
public struct S has copy, drop { f: bool }
public struct Cup<T> has copy, drop, store { item: T }

fun example(c_x: Cup<u64>, c_s: Cup<S>) {
    // 有效,'Cup<u64>'具有'copy',因为'u64'具有'copy'
    let c_x2 = copy c_x;
    // 有效,'Cup<S>'具有'copy',因为'S'具有'copy'
    let c_s2 = copy c_s;
}

fun invalid(c_account: Cup<signer>, c_n: Cup<NoAbilities>) {
    // 无效,'Cup<signer>'不具有'copy'。
    // 即使'Cup'声明了copy,实例也不具有'copy',
    // 因为'signer'不具有'copy'
    let c_account2 = copy c_account;
    // 无效,'Cup<NoAbilities>'不具有'copy',
    // 因为'NoAbilities'不具有'copy'
    let c_n2 = copy c_n;
}

示例:条件drop

public struct NoAbilities {}
public struct S has copy, drop { f: bool }
public struct Cup<T> has copy, drop, store { item: T }

fun unused() {
    Cup<bool> { item: true }; // 有效,'Cup<bool>'具有'drop'
    Cup<S> { item: S { f: false }}; // 有效,'Cup<S>'具有'drop'
}

fun left_in_local(c_account: Cup<signer>): u64 {
    let c_b = Cup<bool> { item: true };
    let c_s = Cup<S> { item: S { f: false }};
    // 有效返回:'c_account'、'c_b'和'c_s'具有值
    // 但'Cup<signer>'、'Cup<bool>'和'Cup<S>'具有'drop'
    0
}

fun invalid_unused() {
    // 无效,不能忽略'Cup<NoAbilities>',因为它不具有'drop'。
    // 即使'Cup'声明了'drop',实例也不具有'drop',
    // 因为'NoAbilities'不具有'drop'
    Cup<NoAbilities> { item: NoAbilities {} };
}

fun invalid_left_in_local(): u64 {
    let n = Cup<NoAbilities> { item: NoAbilities {} };
    // 无效返回:'c_n'具有一个值
    // 而'Cup<NoAbilities>'不具有'drop'
    0
}

示例: 条件性 store 能力

public struct Cup<T> has copy, drop, store { item: T }

// 'MyInnerData' 声明时带有 'store' 能力,所以所有字段都需要 'store' 能力
struct MyInnerData has store {
    yes: Cup<u64>, // 有效,因为 'Cup<u64>' 有 'store' 能力
    // no: Cup<signer>, 无效,因为 'Cup<signer>' 没有 'store' 能力
}

// 'MyData' 声明时带有 'key' 能力,所以所有字段都需要 'store' 能力
struct MyData has key {
    yes: Cup<u64>, // 有效,因为 'Cup<u64>' 有 'store' 能力
    inner: Cup<MyInnerData>, // 有效,因为 'Cup<MyInnerData>' 有 'store' 能力
    // no: Cup<signer>, 无效,因为 'Cup<signer>' 没有 'store' 能力
}

示例: 条件性 key 能力

public struct NoAbilities {}
public struct MyData<T> has key { f: T }

fun valid(addr: address) acquires MyData {
    // 有效,因为 'MyData<u64>' 有 'key' 能力
    transfer(addr, MyData<u64> { f: 0 });
}

fun invalid(addr: address) {
   // 无效,因为 'MyData<NoAbilities>' 没有 'key' 能力
   transfer(addr, MyData<NoAbilities> { f: NoAbilities {} })
   // 无效,因为 'MyData<NoAbilities>' 没有 'key' 能力
   borrow<NoAbilities>(addr);
   // 无效,因为 'MyData<NoAbilities>' 没有 'key' 能力
   borrow_mut<NoAbilities>(addr);
}

// 模拟存储操作
native public fun transfer<T: key>(addr: address, value: T);

Uses and Aliases

use 语法可用于创建对其他模块成员的别名。use 可用于创建在整个模块或给定表达式块范围内的别名。

语法

use 有几种不同的语法情况。首先是最简单的情况,我们可以创建其他模块的别名:

use <address>::<module name>;
use <address>::<module name> as <module alias name>;

例如:

use std::vector;
use std::option as o;

use std::vector; 引入了 vector 作为 std::vector 的别名。这意味着在 use 作用范围内,您可以使用 vector 来代替 std::vectoruse std::vector; 等价于 use std::vector as vector;

同样,use std::option as o; 允许您使用 o 来代替 std::option

use std::vector;
use std::option as o;

fun new_vec(): vector<o::Option<u8>> {
    let mut v = vector[];
    vector::push_back(&mut v, o::some(0));
    vector::push_back(&mut v, o::none());
    v
}

如果您想导入特定的模块成员(例如函数或结构),可以使用以下语法:

use <address>::<module name>::<module member>;
use <address>::<module name>::<module member> as <member alias>;

例如:

use std::vector::push_back;
use std::option::some as s;

这将允许您在不使用完整限定名称的情况下使用函数 std::vector::push_back。同样,对于 std::option::some,您可以使用 suse std::vector::push_back; 等价于 use std::vector::push_back as push_back;

use std::vector::push_back;
use std::option::some as s;

fun new_vec(): vector<std::option::Option<u8>> {
    let mut v = vector[];
    vector::push_back(&mut v, s(0));
    vector::push_back(&mut v, std::option::none());
    v
}

多个别名

如果您想一次为多个模块成员添加别名,可以使用以下语法:

use <address>::<module name>::{<module member>, <module member> as <member alias> ... };

例如:

use std::vector::push_back;
use std::option::{some as s, none as n};

fun new_vec(): vector<std::option::Option<u8>> {
    let mut v = vector[];
    push_back(&mut v, s(0));
    push_back(&mut v, n());
    v
}

Self 别名

如果您需要为模块本身添加别名以及模块成员,可以在单个 use 中使用 SelfSelf 是一种成员,指的是模块本身。

use std::option::{Self, some, none};

为了清楚起见,以下所有内容都是等价的:

use std::option;
use std::option as option;
use std::option::Self;
use std::option::Self as option;
use std::option::{Self};
use std::option::{Self as option};

同一定义的多个别名

如果需要,您可以为任何项目创建任意多个别名。

use std::vector::push_back;
use std::option::{Option, some, none};

fun new_vec(): vector<Option<u8>> {
    let mut v = vector[];
    push_back(&mut v, some(0));
    push_back(&mut v, none());
    v
}

嵌套导入

在 Move 中,您还可以使用同一个 use 声明导入多个名称。这会将所有提供的名称带入作用范围:

use std::{
    vector::{Self as vec, push_back},
    string::{String, Self as str}
};

fun example(s: &mut String) {
    let mut v = vec::empty();
    push_back(&mut v, 0);
    push_back(&mut v, 10);
    str::append_utf8(s, v);
}

module 内部

module 内部,无论声明顺序如何,所有 use 声明都是可用的。

module a::example {
    use std::vector;

    fun new_vec(): vector<Option<u8>> {
        let mut v = vector[];
        vector::push_back(&mut v, 0);
        vector::push_back(&mut v, 10);
        v
    }

    use std::option::{Option, some, none};
}

模块中声明的 use 别名在该模块内是可用的。

此外,引入的别名不能与其他模块成员冲突。有关详细信息,请参见唯一性

在表达式内部

您可以在任何表达式块的开头添加 use 声明。

module a::example {
    fun new_vec(): vector<Option<u8>> {
        use std::vector::push_back;
        use std::option::{Option, some, none};

        let mut v = vector[];
        push_back(&mut v, some(0));
        push_back(&mut v, none());
        v
    }
}

let 类似,在表达式块中引入的 use 别名会在该块结束时被移除。

module a::example {
    fun new_vec(): vector<Option<u8>> {
        let result = {
            use std::vector::push_back;
            use std::option::{Option, some, none};

            let mut v = vector[];
            push_back(&mut v, some(0));
            push_back(&mut v, none());
            v
        };
        result
    }
}

尝试在块结束后使用别名会导致错误。

    fun new_vec(): vector<Option<u8>> {
        let mut result = {
            use std::vector::push_back;
            use std::option::{Option, some, none};

            let mut v = vector[];
            push_back(&mut v, some(0));
            v
        };
        push_back(&mut result, std::option::none());
        // ^^^^^^ 错误! 未绑定函数 'push_back'
        result
    }

任何 use 必须是块中的第一个项目。如果 use 出现在任何表达式或 let 之后,则会导致解析错误。

{
    let mut v = vector[];
    use std::vector; // 错误!
}

这允许您在许多情况下缩短导入块。请注意,这些导入和前面的导入都受限于以下部分中描述的命名和唯一性规则。

命名规则

别名必须遵循与其他模块成员相同的规则。这意味着结构体(和常量)的别名必须以 AZ 开头。

module a::data {
    public struct S {}
    const FLAG: bool = false;
    public fun foo() {}
}
module a::example {
    use a::data::{
        S as s, // 错误!
        FLAG as fLAG, // 错误!
        foo as FOO,  // 有效
        foo as bar, // 有效
    };
}

唯一性

在给定范围内,由 use 声明引入的所有别名必须是唯一的。

对于模块来说,这意味着 use 引入的别名不能重叠。

module a::example {
    use std::option::{none as foo, some as foo}; // 错误!
    //                                     ^^^ 重复的 'foo'

    use std::option::none as bar;

    use std::option::some as bar; // 错误!
    //                       ^^^ 重复的 'bar'

}

此外,它们不能与模块的其他成员重叠。

module a::data {
    public struct S {}
}
module example {
    use a::data::S;

    public struct S { value: u64 } // 错误!
    //            ^ 与上面的别名 'S' 冲突
}
}

在表达式块内部,它们不能彼此重叠,但它们可以 遮蔽 外部范围的其他别名或名称。

遮蔽

表达式块内的 use 别名可以遮蔽外部范围的名称(模块成员或别名)。与本地变量的遮蔽一样,遮蔽在表达式块结束时结束。

module a::example {

    public struct WrappedVector { vec: vector<u64> }

    public fun empty(): WrappedVector {
        WrappedVector { vec: std::vector::empty() }
    }

    public fun push_back(v: &mut WrappedVector, value: u64) {
        std::vector::push_back(&mut v.vec, value);
    }

    fun example1(): WrappedVector {
        use std::vector::push_back;
        // 'push_back' 现在指向 std::vector::push_back
        let mut vec = vector[];
        push_back(&mut vec, 0);
        push_back(&mut vec, 1);
        push_back(&mut vec, 10);
        WrappedVector { vec }
    }

    fun example2(): WrappedVector {
        let vec = {
            use std::vector::push_back;
            // 'push_back' 现在指向 std::vector::push_back

            let mut v = vector[];
            push_back(&mut v, 0);
            push_back(&mut v, 1);
            v
        };
        // 'push_back' 现在指向 Self::push_back
        let mut res = WrappedVector { vec };
        push_back(&mut res, 10);
        res
    }
}

未使用的 Use 或别名

未使用的 use 会导致警告。

module a::example {
    use std::option::{some, none}; // 警告!
    //                      ^^^^ 未使用的别名 'none'

    public fun example(): std::option::Option<u8> {
        some(0)
    }
}

Methods(方法)

为了简化语法,Move中的一些函数可以作为值的“方法”来调用。这是通过使用 . 操作符来调用函数实现的,其中 . 左侧的值是函数的第一个参数(有时称为接收者)。该值的类型静态决定了调用哪个函数。这与其他一些语言的重要区别在于,这种语法表示的不是动态调用,函数调用在运行时确定。在Move中,所有函数调用都是静态确定的。

简而言之,这种语法的存在使得在调用函数时不必创建 use 别名,并且不必显式借用函数的第一个参数。此外,这可以使代码更具可读性,因为它减少了调用函数所需的样板代码,并使得链式调用函数更容易。

语法

调用方法的语法如下:

<expression> . <identifier> <[type_arguments],*> ( <arguments> )

例如:

coin.value();
*nums.borrow_mut(i) = 5;

方法解析

当调用方法时,编译器将静态确定调用哪个函数,基于接收者的类型(. 左侧的参数)。编译器维护一个从类型和方法名称到模块和函数名称的映射。这个映射是根据当前范围内的 use fun 别名以及接收者类型的定义模块中的适当函数创建的。在所有情况下,接收者类型是函数的第一个参数,无论是按值传递还是按引用传递。

在本节中,当我们说一个方法“解析”为一个函数时,我们的意思是编译器将静态地用正常的函数调用替换该方法。例如,如果我们有 x.foo(e),其中 foo 解析为 a::m::foo,则编译器将 x.foo(e) 替换为 a::m::foo(x, e),可能会自动借用 x

定义模块中的函数

在类型的定义模块中,编译器将自动为其类型的任何函数声明创建一个方法别名,当类型是函数的第一个参数时。例如:

module a::m {
    public struct X() has copy, drop, store;
    public fun foo(x: &X) { ... }
    public fun bar(flag: bool, x: &X) { ... }
}

函数 foo 可以作为类型 X 的值的方法调用。然而,bar 不能(因为第一个参数不是 X,并且不会为 bool 创建一个别名,因为 bool 不是在该模块中定义的)。例如:

fun example(x: a::m::X) {
    x.foo(); // 有效
    // x.bar(true); 错误!
}

use fun 别名

与传统的 use 语句类似,use fun 语句创建一个别名,在其当前范围内是局部的。这可以是当前模块或当前表达式块。然而,该别名是关联到一个类型的。

use fun 语句的语法如下:

use fun <function> as <type>.<method alias>;

这为 <function> 创建了一个别名, 可以作为 <method alias> 接收。

例如:

module a::cup {
    public struct Cup<T>(T) has copy, drop, store;

    public fun cup_borrow<T>(c: &Cup<T>): &T {
        &c.0
    }

    public fun cup_value<T>(c: Cup<T>): T {
        let Cup(t) = c;
        t
    }

    public fun cup_swap<T: drop>(c: &mut Cup<T>, t: T) {
        c.0 = t;
    }
}

我们现在可以为这些函数创建 use fun 别名:

module b::example {
    use fun a::cup::cup_borrow as Cup.borrow;
    use fun a::cup::cup_value as Cup.value;
    use fun a::cup::cup_swap as Cup.set;

    fun example(c: &mut Cup<u64>) {
        let _ = c.borrow(); // 解析为 a::cup::cup_borrow
        let v = c.value(); // 解析为 a::cup::cup_value
        c.set(v * 2); // 解析为 a::cup::cup_swap
    }
}

注意,use fun 中的 <function> 不必是一个完全解析的路径,可以使用别名,因此上述示例中的声明也可以等效地写成:

    use a::cup::{Self, cup_swap};

    use fun cup::cup_borrow as Cup.borrow;
    use fun cup::cup_value as Cup.value;
    use fun cup_swap as Cup.set;

虽然这些示例在当前模块中重命名函数很有趣,但该功能在声明其他模块中的类型的方法时可能更有用。例如,如果我们想向 Cup 添加一个新实用程序,我们可以使用 use fun 别名并仍然使用方法语法:

module b::example {

    fun double(c: &Cup<u64>): Cup<u64> {
        let v = c.value();
        Cup::new(v * 2)
    }

}

通常,我们不得不调用 double(&c),因为 b::example 并未定义 Cup,但我们可以使用 use fun 别名:

    fun double_double(c: Cup<u64>): (Cup<u64>, Cup<u64>) {
        use fun b::example::double as Cup.dub;
        (c.dub(), c.dub()) // 在两个调用中都解析为 b::example::double
    }

虽然 use fun 可以在任何范围内创建,但 use fun 的目标 <function> 必须具有与 <type> 相同的第一个参数。

public struct X() has copy, drop, store;

fun new(): X { X() }
fun flag(flag: bool): u8 { if (flag) 1 else 0 }

use fun new as X.new; // 错误!
use fun flag as X.flag; // 错误!
// `new` 和 `flag` 都没有第一个参数为类型 `X`

<type> 的任何第一个参数都可以使用,包括引用和可变引用:

public struct X() has copy, drop, store;

public fun by_val(_: X) {}
public fun by_ref(_: &X) {}
public fun by_mut(_: &mut X) {}

// 全部3个有效,在任何范围内
use fun by_val as X.v;
use fun by_ref as X.r;
use fun by_mut as X.m;

注意对于泛型,方法是关联到该泛型类型的所有实例的。不能根据实例化情况重载方法以解析为不同的函数。

public struct Cup<T>(T) has copy, drop, store;

public fun value<T: copy>(c: &Cup<T>): T {
    c.0
}

use fun value as Cup<bool>.flag; // 错误!
use fun value as Cup<u64>.num; // 错误!
// 在这两种情况下,`use fun` 别名不能是泛型的,它们必须对该类型的所有实例有效

public use fun 别名

与传统的 use 不同,use fun 语句可以被声明为 public,允许在其声明的作用域之外使用。如果在定义接收者类型的模块中声明,use fun 可以是 public 的,这类似于在定义模块中为函数自动创建的方法别名。或者可以认为,对于每个在定义模块中定义的第一个参数为接收者类型的函数,自动创建一个隐式的 public use fun。这两种观点是等价的。

module a::cup {
    public struct Cup<T>(T) has copy, drop, store;

    public use fun cup_borrow as Cup.borrow;
    public fun cup_borrow<T>(c: &Cup<T>): &T {
        &c.0
    }
}

在此示例中,为 a::cup::Cup.borrowa::cup::Cup.cup_borrow 创建了一个公共方法别名。两者都解析为 a::cup::cup_borrow,并且两者都是“公共的”,这意味着它们可以在 a::cup 之外使用,而无需额外的 useuse fun

module b::example {

    fun example<T: drop>(c: a::cup::Cup<u64>) {
        c.borrow(); // 解析为 a::cup::cup_borrow
        c.cup_borrow(); // 解析为 a::cup::cup_borrow
    }
}

public use fun 声明因此作为一种重命名函数的方法,如果你想为方法语法提供一个更简洁的名称。这在具有多种类型且每种类型具有类似名称的函数的模块中特别有用。

module a::shapes {

    public struct Rectangle { base: u64, height: u64 }
    public struct Box { base: u64, height: u64, depth: u64 }

    // Rectangle 和 Box 可以具有相同名称的方法

    public use fun rectangle_base as Rectangle.base;
    public fun rectangle_base(rectangle: &Rectangle): u64 {
        rectangle.base
    }

    public use fun box_base as Box.base;
    public fun box_base(box: &Box): u64 {
        box.base
    }

}

public use fun 的另一个用途是为其他模块中的类型添加方法。这在单个包中跨多个模块分散的函数中很有用。

module a::cup {
    public struct Cup<T>(T) has copy, drop, store;

    public fun new<T>(t: T): Cup<T> { Cup(t) }
    public fun borrow<T>(c: &Cup<T>): &T {
        &c.0
    }
    // `public use fun` 引用在另一个模块中定义的函数
    public use fun a::utils::split as Cup.split;
}

module a::utils {
    use a::m::{Self, Cup};

    public fun split<u64>(c: Cup<u64>): (Cup<u64>, Cup<u64>) {
        let Cup(t) = c;
        let half = t / 2;
        let rem = if (t > 0) t - half else 0;
        (cup::new(half), cup::new(rem))
    }

}

需要注意的是,这个 public use fun 不会创建循环依赖,因为 use fun 在模块编译后不存在--所有方法都是静态解析的。

use 别名的交互

需要注意的一点是,方法别名遵循正常的 use 别名规则。

module a::cup {
    public struct Cup<T>(T) has copy, drop, store;

    public fun cup_borrow<T>(c: &Cup<T>): &T {
        &c.0
    }
}

module b::other {
    use a::cup::{Cup, cup_borrow as borrow};

    fun example(c: &Cup<u64>) {
        c.borrow(); // 解析为 a::cup::cup_borrow
    }
}

一个有用的思路是,use 会在可能的情况下为函数创建一个隐式的 use fun 别名。在这种情况下,use a::cup::cup_borrow as borrow 创建了一个隐式的 use fun a::cup::cup_borrow as Cup.borrow,因为它是一个有效的 use fun 别名。这两种观点是等价的。这种推理可以帮助理解特定方法在遮蔽情况下如何解析。有关更多详细信息,请参见作用域中的案例。

作用域

如果不是 public 的,use fun 别名是其作用域的局部变量,就像普通的use一样。例如:

module a::m {
    public struct X() has copy, drop, store;
    public fun foo(_: &X) {}
    public fun bar(_: &X) {}
}

module b::other {
    use a::m::X;

    use fun a::m::foo as X.f;

    fun example(x: &X) {
        x.f(); // 解析为 a::m::foo
        {
            use a::m::bar as f;
            x.f(); // 解析为 a::m::bar
        };
        x.f(); // 仍然解析为 a::m::foo
        {
            use fun a::m::bar as X.f;
            x.f(); // 解析为 a::m::bar
        }
    }

自动借用

在解析方法时,编译器会在函数需要引用时自动借用接收者。例如:

module a::m {
    public struct X() has copy, drop;
    public fun by_val(_: X) {}
    public fun by_ref(_: &X) {}
    public fun by_mut(_: &mut X) {}

    fun example(mut x: X) {
        x.by_ref(); // 解析为 a::m::by_ref(&x)
        x.by_mut(); // 解析为 a::m::by_mut(&mut x)
    }
}

在这些示例中,x 被自动借用为 &x&mut x。这也适用于字段访问:

module a::m {
    public struct X() has copy, drop;
    public fun by_val(_: X) {}
    public fun by_ref(_: &X) {}
    public fun by_mut(_: &mut X) {}

    public struct Y has drop { x: X }

    fun example(mut y: Y) {
        y.x.by_ref(); // 解析为 a::m::by_ref(&y.x)
        y.x.by_mut(); // 解析为 a::m::by_mut(&mut y.x)
    }
}

请注意,在这两个示例中,本地变量必须标记为 mut 以允许 &mut 借用。否则,会出现错误,提示 x(或第二个示例中的 y)不可变。

需要记住的是,如果没有引用,正常的变量和字段访问规则将生效。这意味着如果没有借用,值可能会被移动或复制。

module a::m {
    public struct X() has copy, drop;
    public fun by_val(_: X) {}
    public fun by_ref(_: &X) {}
    public fun by_mut(_: &mut X) {}

    public struct Y has drop { x: X }
    public fun drop_y(y: Y) { y }

    fun example(y: Y) {
        y.x.by_val(); // 复制 `y.x` 因为 `by_val` 是按值传递的,并且 `X` 具有 `copy`
        y.drop_y(); // 移动 `y` 因为 `drop_y` 是按值传递的,并且 `Y` 没有 `copy`
    }
}

链式调用

方法调用可以是链式调用,因为任何表达式都可以是方法的接收者。

module a::shapes {
    public struct Point has copy, drop, store { x: u64, y: u64 }
    public struct Line has copy, drop, store { start: Point, end: Point }

    public fun x(p: &Point): u64 { p.x }
    public fun y(p: &Point): u64 { p.y }

    public fun start(l: &Line): &Point { &l.start }
    public fun end(l: &Line): &Point { &l.end }

}

module b::example {
    use a::shapes::Line;

    public fun x_values(l: Line): (u64, u64) {
        (l.start().x(), l.end().x())
    }

}

在这个例子中,对于 l.start().x(),编译器首先将 l.start() 解析为 a::shapes::start(&l)。然后将 .x() 解析为 a::shapes::x(a::shapes::start(&l))。同样适用于 l.end().x()。请记住,这个功能并不是"特别的"--. 左边的表达式可以是任何表达式,编译器将正常解析方法调用。我们特别提到这种"链式调用",因为它是增加可读性的常见做法。

Index Syntax

Move 提供了语法属性,允许您定义操作,使这些操作看起来和感觉像原生 Move 代码,从而将这些操作降低到您提供的定义中。

我们的第一个语法方法 index 允许您定义一组操作,可以用作自定义索引访问器,例如通过注释应该用于这些索引操作的函数来访问矩阵元素 m[i,j]。此外,这些定义是每种类型专属的,并且可以隐式地供任何使用您的类型的程序员使用。

概述和总结

首先,考虑一个使用向量的向量表示其值的 Matrix 类型。您可以使用 index 语法注解在 borrowborrow_mut 函数上编写一个小型库,如下所示:

module matrix {

    public struct Matrix<T> { v: vector<vector<T>> }

    #[syntax(index)]
    public fun borrow<T>(s: &Matrix<T>, i: u64, j: u64): &T {
        vector::borrow(vector::borrow(&s.v, i), j)
    }

    #[syntax(index)]
    public fun borrow_mut<T>(s: &mut Matrix<T>, i: u64, j: u64): &mut T {
        vector::borrow_mut(vector::borrow_mut(&mut s.v, i), j)
    }

    public fun make_matrix<T>(v: vector<vector<T>>):  Matrix<T> {
        Matrix { v }
    }

}

现在,任何使用此 Matrix 类型的人都可以使用其索引语法:

let mut m = matrix::make_matrix(vector[
    vector[1, 0, 0],
    vector[0, 1, 0],
    vector[0, 0, 1],
]);

let mut i = 0;
while (i < 3) {
    let mut j = 0;
    while (j < 3) {
        if (i == j) {
            assert!(m[i, j] == 1, 1);
        } else {
            assert!(m[i, j] == 0, 0);
        };
        *(&mut m[i,j]) = 2;
        j = j + 1;
    };
    i = i + 1;
}

用法

如示例所示,如果定义了数据类型和相关的索引语法方法,任何人都可以通过在该类型的值上编写索引语法来调用该方法:

let mat = matrix::make_matrix(...);
let m_0_0 = mat[0, 0];

在编译期间,编译器会根据表达式的位置和可变性将这些转换为相应的函数调用:

let mut mat = matrix::make_matrix(...);

let m_0_0 = mat[0, 0];
// 翻译为 `copy matrix::borrow(&mat, 0, 0)`

let m_0_0 = &mat[0, 0];
// 翻译为 `matrix::borrow(&mat, 0, 0)`

let m_0_0 = &mut mat[0, 0];
// 翻译为 `matrix::borrow_mut(&mut mat, 0, 0)`

您还可以将索引表达式与字段访问混合使用:

public struct V { v: vector<u64> }

public struct Vs { vs: vector<V> }

fun borrow_first(input: &Vs): &u64 {
    &input.vs[0].v[0]
    // 翻译为 `vector::borrow(&vector::borrow(&input.vs, 0).v, 0)`
}

索引函数接受灵活参数

请注意,除了本章其余部分描述的定义和类型限制外,Move 不对您的索引语法方法接受的参数值施加限制。这允许您在定义索引语法时实现复杂的程序行为,例如一个在索引超出范围时接受默认值的数据结构:

#[syntax(index)]
public fun borrow_or_set<Key: copy, Value: drop>(
    input: &mut MTable<Key, Value>,
    key: Key,
    default: Value
): &mut Value {
    if (contains(input, key)) {
        borrow(input, key)
    } else {
        insert(input, key, default);
        borrow(input, key)
    }
}

现在,当您索引 MTable 时,还必须提供一个默认值:

let string_key: String = ...;
let mut table: MTable<String, u64> = m_table::make_table();
let entry: &mut u64 = &mut table[string_key, 0];

这种扩展能力允许您为您的类型编写精确的索引接口,从而具体执行自定义行为。

定义索引语法函数

这种强大的语法形式允许您定义的所有数据类型都以这种方式运行,假设您的定义遵循以下规则:

  1. #[syntax(index)] 属性添加到在与主题类型相同的模块中定义的指定函数中。
  2. 指定的函数具有 public 可见性。
  3. 函数接受一个引用类型作为其主题类型(其第一个参数),并返回一个匹配的引用类型(如果主题是 mut,则返回 mut)。
  4. 每种类型仅有一个可变和一个不可变的定义。
  5. 不可变和可变版本具有类型一致性:
    • 主题类型匹配,仅在可变性上有所不同。
    • 返回类型与其主题类型的可变性匹配。
    • 如果存在类型参数,则在两个版本之间具有相同的约束。
    • 除主题类型外的所有参数都相同。

以下内容和附加示例更详细地描述了这些规则。

声明

要声明索引语法方法,请在相关函数定义上方添加 #[syntax(index)] 属性,该函数定义在与主题类型定义相同的模块中。这向编译器表明该函数是指定类型的索引访问器。

不可变访问器

不可变索引语法方法是为只读访问定义的。它接受主题类型的不可变引用,并返回元素类型的不可变引用。在 std::vector 中定义的 borrow 函数是一个示例:

#[syntax(index)]
public native fun borrow<Element>(v: &vector<Element>, i: u64): &Element;

可变访问器

可变索引语法方法是不可变方法的对偶,允许进行读写操作。它接受主题类型的可变引用,并返回元素类型的可变引用。在 std::vector 中定义的 borrow_mut 函数是一个示例:

#[syntax(index)]
public native fun borrow_mut<Element>(v: &mut vector<Element>, i: u64): &mut Element;

可见性

为了确保索引函数在类型使用的任何地方都可用,所有索引语法方法必须具有公共可见性。这确保了跨 Move 模块和包的索引使用的便捷性。

无重复

除了上述要求外,我们还限制每个主题基类型定义一个不可变引用和一个可变引用的索引语法方法。例如,您不能为多态类型定义专门版本:

#[syntax(index)]
public fun borrow_matrix_u64(s: &Matrix<u64>, i: u64, j: u64): &u64 { ... }

#[syntax(index)]
public fun borrow_matrix<T>(s: &Matrix<T>, i: u64, j: u64): &T { ... }
// 错误!Matrix 已经有了不可变索引语法方法的定义

这确保了您始终可以知道正在调用哪个方法,而无需检查类型实例化。

类型约束

默认情况下,索引语法方法具有以下类型约束:

其主题类型(第一个参数)必须是指向同一模块中定义的单个类型的引用。这意味着您不能为元组、类型参数或值定义索引语法方法:

#[syntax(index)]
public fun borrow_fst(x: &(u64, u64), ...): &u64 { ... }
    // 错误,因为主题类型是元组

#[syntax(index)]
public fun borrow_tyarg<T>(x: &T, ...): &T { ... }
    // 错误,因为主题类型是类型参数

#[syntax(index)]
public fun borrow_value(x: Matrix<u64>, ...): &u64 { ... }
    // 错误,因为x不是引用

主题类型必须与返回类型具有相同的可变性。 这个限制允许您在借用索引表达式作为&vec[i]&mut vec[i]时,明确预期的行为。Move编译器使用可变性标记来确定调用哪种借用形式以生成相应可变性的引用。因此,我们不允许主题和返回可变性不同的索引语法方法:

#[syntax(index)]
public fun borrow_imm(x: &mut Matrix<u64>, ...): &u64 { ... }
    // 错误!不兼容的可变性
    // 预期的是可变引用 '&mut' 返回类型

类型兼容性

当定义一个不可变和可变的索引语法方法对时,它们需要满足一些兼容性约束:

  1. 它们必须接受相同数量的类型参数,并且这些类型参数必须具有相同的约束。
  2. 类型参数必须按位置而不是名称使用。
  3. 除了可变性之外,它们的主题类型必须完全匹配。
  4. 除了可变性之外,它们的返回类型必须完全匹配。
  5. 所有其他参数类型必须完全匹配。

这些约束旨在确保无论是在可变还是不可变位置,索引语法的行为都是相同的。

为了说明其中一些错误,请回顾之前的Matrix定义:

#[syntax(index)]
public fun borrow<T>(s: &Matrix<T>, i: u64, j: u64): &T {
    vector::borrow(vector::borrow(&s.v, i), j)
}

以下所有定义的可变版本都是类型不兼容的:

#[syntax(index)]
public fun borrow_mut<T: drop>(s: &mut Matrix<T>, i: u64, j: u64): &mut T { ... }
    // 错误!这里`T`有`drop`,但不在不可变版本中

#[syntax(index)]
public fun borrow_mut(s: &mut Matrix<u64>, i: u64, j: u64): &mut u64 { ... }
    // 错误!这里使用了不同数量的类型参数

#[syntax(index)]
public fun borrow_mut<T, U>(s: &mut Matrix<U>, i: u64, j: u64): &mut U { ... }
    // 错误!这里使用了不同数量的类型参数

#[syntax(index)]
public fun borrow_mut<U>(s: &mut Matrix<U>, i_j: (u64, u64)): &mut U { ... }
    // 错误!这里使用了不同数量的参数

#[syntax(index)]
public fun borrow_mut<U>(s: &mut Matrix<U>, i: u64, j: u32): &mut U { ... }
    // 错误!`j`是不同的类型

再次强调,目标是使不可变版本和可变版本之间的使用方式一致。这样一来,索引语法方法可以在可变和不可变使用时都能正常工作,而不会根据可变性改变行为或约束,最终确保具有一致的可编程接口。

Packages(包)

包允许Move程序员更轻松地重用代码,并在项目之间共享代码。Move包系统使程序员能够轻松地:

  • 定义包含Move代码的包;
  • 通过命名地址对包进行参数化;
  • 在其他Move代码中导入和使用包,并实例化命名地址;
  • 构建包并生成与包相关的编译工件;
  • 在编译后与编译的Move工件周围使用公共接口。

包布局和清单语法

Move包源目录包含一个 Move.toml 包清单文件,一个生成的 Move.lock 文件,以及一组子目录:

a_move_package
├── Move.toml      (必需)
├── Move.lock      (生成的)
├── sources        (必需)
├── doc_templates  (可选)
├── examples       (可选,测试和开发模式)
└── tests          (可选,测试模式)

标记为 "必需" 的目录和文件必须存在,才能被视为一个Move包并进行构建。可选目录可以存在,如果存在,则根据构建包的模式将其包含在编译过程中。例如,在“dev”或“test”模式下构建时,testsexamples 目录也会被包含进来。

依次查看每个内容:

  1. Move.toml 文件是包清单,是一个Move包被视为必需的文件。该文件包含关于包的元数据,如名称、依赖关系等。
  2. Move.lock 文件由Move CLI生成,包含了包及其依赖项的固定构建版本。它用于确保在不同的构建中使用一致的版本,并确保依赖项的更改在该文件中体现为变更。
  3. sources 目录是必需的,包含构成包的Move模块。此目录中的模块将始终包含在编译过程中。
  4. doc_templates 目录可以包含文档模板,用于生成包的文档。
  5. examples 目录可以包含额外的代码,仅用于开发和/或教程,这些内容不会在非testdev模式下编译。
  6. tests 目录可以包含仅在test模式下编译或运行Move单元测试的Move模块。

Move.toml

Move包清单在 Move.toml 文件中定义,具有以下语法。可选字段用 * 标记,+ 表示一个或多个元素:

[package]
name = <string>
edition* = <string>      # 例如,"2024.alpha" 表示使用Move 2024版,目前处于alpha阶段。如果未指定,则默认为最新的稳定版。
license* = <string>              # 例如,"MIT", "GPL", "Apache 2.0"
authors* = [<string>,+]  # 例如,["Joe Smith (joesmith@noemail.com)", "John Snow (johnsnow@noemail.com)"]

# 外部工具可以向该部分添加额外的字段。例如,在Sui上,会添加以下部分:
published-at* = "<hex-address>" # 包的发布地址。应在第一次发布后设置。

[dependencies] # (可选部分)依赖项的路径
# 一个或多个行声明依赖项,格式如下

# ##### 本地依赖项 #####
# 对于本地依赖项,请使用 `local = path`。路径相对于包根目录
# Local = { local = "../path/to" }
# 若要解决版本冲突并强制指定依赖项的特定版本,可以使用 `override = true`
# Override = { local = "../conflicting/version", override = true }
# 要对依赖项中的地址值进行实例化,请使用 `addr_subst`
<string> = {
    local = <string>,
    override* = <bool>,
    addr_subst* = { (<string> = (<string> | "<hex_address>"))+ }
}

# ##### Git依赖项 #####
# 对于远程导入,请使用 `{ git = "...", subdir = "...", rev = "..." }`。
# 必须提供修订版,可以是分支、标签或提交哈希。
# 如果未指定 `subdir`,则使用存储库的根目录。
# MyRemotePackage = { git = "https://some.remote/host.git", subdir = "remote/path", rev = "main" }
<string> = {
    git = <URL ending in .git>,
    subdir=<path to dir containing Move.toml inside git repo>,
    rev=<git commit hash>,
    override* = <bool>,
    addr_subst* = { (<string> = (<string> | "<hex_address>"))+ }
}

[addresses]  # (可选部分)在此包中声明命名地址
# 一个或多个行声明命名地址,格式如下
# 与包名称匹配的地址必须设置为 `"0x0"`,否则将无法发布。
<addr_name> = "_" | "<hex_address>" # 例如,std = "_" 或 my_addr = "0xC0FFEECAFE"

# 命名地址在Move中可以使用 `@name` 访问。它们也可以被导出:
# 例如,`std = "0x1"` 是标准库导出的。
# alice = "0xA11CE"

[dev-dependencies] # (可选部分)与 [dependencies] 部分相同,但仅在“dev”和“test”模式下包含
# dev-dependencies 部分允许在“--test”和“--dev”模式下覆盖依赖项。例如,您可以在这里引入仅用于测试的依赖项。
# Local = { local = "../path/to/dev-build" }
<string> = {
    local = <string>,
    override* = <bool>,
    addr_subst* = { (<string> = (<string> | "<hex_address>"))+ }
}
<string> = {
    git = <URL ending in .git>,
    subdir=<path to dir containing Move.toml inside git repo>,
    rev=<git commit hash>,
    override* = <bool>,
    addr_subst* = { (<string> = (<string> | "<hex_address>"))+ }
}

[dev-addresses] # (可选部分)与 [addresses] 部分相同,但仅在“dev”和“test”模式下包含
# dev-addresses 部分允许在“--test”和“--dev”模式下覆盖命名地址。
<addr_name> = "<hex_address>" # 例如,alice = "0xB0B"

一个最小的包清单示例:

[package]
name = "AName"

一个更标准的包清单示例,还包括了Move标准库,并通过LocalDep包将命名地址std实例化为地址值0x1

[package]
name = "AName"
license = "Apache 2.0"

[addresses]
address_to_be_filled_in = "_"
specified_address = "0xB0B"

[dependencies]
# 本地依赖项
LocalDep = { local = "projects/move-awesomeness", addr_subst = { "std" = "0x1" } }
# Git依赖项
MoveStdlib = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/move-stdlib", rev = "framework/mainnet" }

[dev-addresses] # 用于开发该模块时
address_to_be_filled_in = "0x101010101"

大多数包清单中的部分都是不言自明的,但命名地址可能有点难以理解,因此我们将更详细地探讨它们在编译期间的命名地址

编译期间的命名地址

回想一下,Move具有命名地址,而命名地址不能在Move中声明。相反,它们在包级别声明:在Move包的清单文件(Move.toml)中,您可以在包中声明命名地址,在Move包系统中实例化其他命名地址,并重命名来自其他包的命名地址。

让我们逐一浏览每个操作,以及它们如何在包的清单中执行:

声明命名地址

假设我们在 example_pkg/sources/A.move 中有一个Move模块,如下所示:

module named_addr::a {
    public fun x(): address { @named_addr }
}

我们可以在 example_pkg/Move.toml 中两种不同方式声明命名地址 named_addr。首先是:

[package]
name = "example_pkg"
...
[addresses]
named_addr = "_"

named_addr 声明为 example_pkg 包中的命名地址,并且_此地址可以是任何有效的地址值_。特别地,导入包可以选择将命名地址 named_addr 的值设为任何它希望的地址。直观地说,您可以将其视为通过命名地址 named_addr 参数化包 example_pkg,然后可以稍后由导入包实例化。

named_addr 也可以声明为:

[package]
name = "example_pkg"
...
[addresses]
named_addr = "0xCAFE"

这表明命名地址 named_addr 确切地是 0xCAFE,并且不能更改。这对于其他导入包可以使用这个命名地址而无需担心它的确切值非常有用。

有了这两种不同的声明方法,有两种信息关于命名地址可以在包图中流动的方式:

  • 前者("未分配的命名地址")允许命名地址值从导入位置流向声明位置。
  • 后者("已分配的命名地址")允许命名地址值从声明位置向上在包图中流向使用位置。

有了这两种在整个包图中流动命名地址信息的方法,理解作用域和重命名规则变得非常重要。

命名地址的作用域和重命名

在包 P 中,命名地址 N 的作用域遵循以下规则:

  1. 如果 P 自己声明了命名地址 N
  2. 或者,P 的一个传递依赖包声明了命名地址 N,并且在包图中存在从 P 到声明 N 的包的依赖路径,并且没有对 N 进行重命名。

此外,每个包中的命名地址都是公开的。因此,根据上述作用域规则,每个包在被导入时都会带有一组命名地址,例如,如果导入 example_pkg,那么 named_addr 命名地址也会随之导入到作用域中。因此,如果包 P 导入了两个包 P1P2,它们都声明了命名地址 N,那么在包 P 中引用 N 时会出现问题:到底是来自 P1 还是 P2 的 "N"?为了避免命名地址的来源不明确,我们要求包内所有依赖引入的作用域集合是不重叠的,并且提供一种在导入包时对命名地址进行 重命名 的方式。

在上面的例子中,可以通过以下方式在 PP1P2 中进行命名地址重命名:

[package]
name = "P"
...
[dependencies]
P1 = { local = "some_path_to_P1", addr_subst = { "P1N" = "N" } }
P2 = { local = "some_path_to_P2"  }

通过这种重命名,N 将引用来自 P2N,而 P1N 将引用来自 P1N

module N::A {
    public fun x(): address { @P1N }
}

需要注意的是,重命名不是局部的:一旦在包 P 中将命名地址 N 重命名为 N2,所有导入 P 的包将不再看到 N,而只能看到 N2,除非 NP 外部重新引入。这就是本节开头规则 (2) 中指定的作用域规则的原因,即 "包图中从 PN 声明包的依赖路径没有对 N 进行重命名"。

命名地址的实例化

命名地址可以在包图中的多个位置多次实例化,只要始终使用相同的值。如果同一个命名地址(无论是否重命名)在包图中的不同位置使用了不同的值,则会出现错误。

Move 包只有在所有命名地址都解析为值时才能编译。如果包希望公开一个未实例化的命名地址,这就是 [dev-addresses] 部分的部分解决方案。该部分可以为命名地址设置值,但不能引入任何命名地址。此外,只有根包中的 [dev-addresses] 会包含在 dev 模式中。例如,具有以下清单的根包在 dev 模式外部不会编译,因为 named_addr 将未实例化:

[package]
name = "example_pkg"
...
[addresses]
named_addr = "_"

[dev-addresses]
named_addr = "0xC0FFEE"

使用和构建产物

Move 包系统作为 CLI 的一部分提供了一个命令行选项:sui move <command> <command_flags>。除非提供特定路径,否则所有包命令都将在当前封闭的 Move 包中运行。可以通过运行 sui move --help 查找 Move CLI 的所有命令和标志列表。

构建产物

可以使用 CLI 命令来编译包。这将创建一个 build 目录,其中包含与构建相关的产物(包括字节码二进制文件、源代码映射和文档)。build 目录的一般布局如下:

a_move_package
├── BuildInfo.yaml
├── bytecode_modules
│   ├── dependencies
│   │   ├── <dep_pkg_name>
│   │   │   └── *.mv
│   │   ...
│   │   └──  <dep_pkg_name>
│   │       └── *.mv
│   ...
│   └── *.mv
├── docs
│   ├── dependencies
│   │   ├── <dep_pkg_name>
│   │   │   └── *.md
│   │   ...
│   │   └──  <dep_pkg_name>
│   │       └── *.md
│   ...
│   └── *.md
├── source_maps
│   ├── dependencies
│   │   ├── <dep_pkg_name>
│   │   │   └── *.mvsm
│   │   ...
│   │   └──  <dep_pkg_name>
│   │       └── *.mvsm
│   ...
│   └── *.mvsm
└── sources
    ...
    └── *.move
    ├── dependencies
    │   ├── <dep_pkg_name>
    │   │   └── *.move
    │   ...
    │   └──  <dep_pkg_name>
    │       └── *.move
    ...
    └── *.move

Move.lock 文件

当 Move 包构建时,会在 Move 包的根目录生成一个 Move.lock 文件。Move.lock 文件包含有关您的包及其构建配置的信息,并充当 Move 编译器与其他工具(如特定链的命令行界面和第三方包管理器)之间的通信层。

Move.toml 文件类似,Move.lock 文件是一个基于文本的 TOML 文件。但与包清单不同的是,您不应直接编辑 Move.lock 文件。工具链中的进程(如 Move 编译器)会访问和编辑文件,以读取并附加相关信息。此外,您也不应将文件从根目录移动,因为它需要与包清单 Move.toml 在同一级别。

如果您在包的源代码控制中使用了版本控制,建议将与所需构建或发布的包对应的 Move.lock 文件一并提交。这样可以确保每次构建的包都与原始包完全一致,并且构建的变更将以 Move.lock 文件的变更形式体现出来。

[move] 部分

这部分包含了锁文件中所需的核心信息:

  • 锁文件版本(用于向后兼容性检查和将来版本化锁文件更改)。
  • 用于生成此锁文件的 Move.toml 文件的 SHA3-256 哈希值。
  • 所有依赖项的 Move.lock 文件的哈希值。如果没有依赖项,则为空字符串。
  • 依赖项列表。
[move]
version = <string> # 锁文件版本,用于向后兼容性检查。
manifest_digest = <hash> # 用于生成此锁文件的 Move.toml 文件的 SHA3-256 哈希值。
deps_digest = <hash> # 所有依赖项的 Move.lock 文件的 SHA3-256 哈希值。如果没有依赖项,则为空字符串。
dependencies = { (name = <string>)* } # 依赖项列表。如果没有依赖项则不出现。

[move.package] 部分

Move 编译器解析每个依赖项后,将依赖项的位置写入 Move.lock 文件。如果依赖项无法解析,则编译器不会写入 Move.lock 文件,构建将失败。如果所有依赖项解析成功,则 Move.lock 文件将以以下格式包含所有包的传递依赖项的位置(本地和远程):

# ...

[[move.package]]
name = "A"
source = { git = "https://github.com/b/c.git", subdir = "e/f", rev = "a1b2c3" }

[[move.package]]
name = "B"
source = { local = "../local-dep" }

[move.toolchain-version] 部分

如上所述,外部工具可能会向锁文件添加其他字段。例如,Sui 包管理器向锁文件添加了工具链版本信息,可用于链上源代码验证:

# ...

[move.toolchain-version]
compiler-version = <string> # 用于构建包的 Move 编译器的版本,例如 "1.21.0"
edition = <string> # 用于构建包的 Move 语言版本,例如 "2024.alpha"
flavor = <string> # 用于构建包的 Move 编译器的特定版本,例如 "sui"

Unit Tests(单元测试)

Move 的单元测试在 Move 源语言中使用三个注解:

  • #[test] 标记一个函数为测试;
  • #[expected_failure] 标记一个测试预期会失败;
  • #[test_only] 标记一个模块或模块成员(use函数结构体常量)仅用于测试。

这些注解可以放置在任何适当的形式上,并具有任何可见性。每当一个模块或模块成员被注解为 #[test_only]#[test] 时,除非为测试编译,否则它不会包含在已编译的字节码中。

测试注解

#[test] 注解只能放在没有参数的函数上。这个注解将函数标记为由单元测试框架运行的测试。

#[test] // 正确
fun this_is_a_test() { ... }

#[test] // 编译失败,因为测试接受了一个参数
fun this_is_not_correct(arg: u64) { ... }

测试还可以标注为 #[expected_failure]。这个注解标记测试预期会引发错误。有许多选项可以与 #[expected_failure] 注解一起使用,以确保只有在指定条件下失败才会被标记为通过,这些选项在预期失败部分详细介绍。只有具有 #[test] 注解的函数也可以被注解为 #[expected_failure]

以下是一些使用 #[expected_failure] 注解的简单示例:

#[test]
#[expected_failure]
public fun this_test_will_abort_and_pass() { abort 1 }

#[test]
#[expected_failure]
public fun test_will_error_and_pass() { 1/0; }

#[test] // 通过,因为测试失败并且返回预期的中止代码常量。
#[expected_failure(abort_code = ENotFound)] // ENotFound 是模块中定义的常量
public fun test_will_error_and_pass_abort_code() { abort ENotFound }

#[test] // 失败,因为测试失败并返回一个不同于预期的错误。
#[expected_failure(abort_code = my_module::ENotFound)]
public fun test_will_error_and_fail() { 1/0; }

#[test, expected_failure] // 可以在一个属性中有多个注解。此测试将通过。
public fun this_other_test_will_abort_and_pass() { abort 1 }

注意: #[test]#[test_only] 函数也可以调用 entry 函数,无论其可见性如何。

预期失败

可以使用 #[expected_failure] 注解来指定不同类型的错误条件。包括以下几种方式:

1. #[expected_failure(abort_code = <constant>)]

如果测试中止并且返回的常量值与模块中定义的常量相同,则测试通过,否则测试失败。这是测试预期失败的推荐方式。

注意: 你可以在 expected_failure 注解中引用当前模块或包之外的常量。

module pkg_addr::other_module {
    const ENotFound: u64 = 1;
    public fun will_abort() {
        abort ENotFound
    }
}

module pkg_addr::my_module {
    use pkg_addr::other_module;
    const ENotFound: u64 = 1;

    #[test]
    #[expected_failure(abort_code = ENotFound)]
    fun test_will_abort_and_pass() { abort ENotFound }

    #[test]
    #[expected_failure(abort_code = other_module::ENotFound)]
    fun test_will_abort_and_pass() { other_module::will_abort() }

    // 失败:因为我们期望的常量来自错误的模块。
    #[test]
    #[expected_failure(abort_code = ENotFound)]
    fun test_will_abort_and_pass() { other_module::will_abort() }
}

2. #[expected_failure(arithmetic_error, location = <location>)]

这指定了测试预期会在指定位置失败,并引发算术错误(例如,整数溢出,除以零等)。<location> 必须是模块位置的有效路径,例如 Selfmy_package::my_module

module pkg_addr::other_module {
    public fun will_arith_error() { 1/0; }
}

module pkg_addr::my_module {
    use pkg_addr::other_module;
    #[test]
    #[expected_failure(arithmetic_error, location = Self)]
    fun test_will_arith_error_and_pass1() { 1/0; }

    #[test]
    #[expected_failure(arithmetic_error, location = pkg_addr::other_module)]
    fun test_will_arith_error_and_pass2() { other_module::will_arith_error() }

    // 失败:因为我们期望它失败的位置与测试实际失败的位置不同。
    #[test]
    #[expected_failure(arithmetic_error, location = Self)]
    fun test_will_arith_error_and_fail() { other_module::will_arith_error() }
}

3. #[expected_failure(out_of_gas, location = <location>)]

这指定了测试预期会在指定位置失败,并引发耗尽气体错误。<location> 必须是模块位置的有效路径,例如 Selfmy_package::my_module

module pkg_addr::other_module {
    public fun will_oog() { loop {} }
}

module pkg_addr::my_module {
    use pkg_addr::other_module;
    #[test]
    #[expected_failure(out_of_gas, location = Self)]
    fun test_will_oog_and_pass1() { loop {} }

    #[test]
    #[expected_failure(arithmetic_error, location = pkg_addr::other_module)]
    fun test_will_oog_and_pass2() { other_module::will_oog() }

    // 失败:因为我们期望它失败的位置与测试实际失败的位置不同。
    #[test]
    #[expected_failure(out_of_gas, location = Self)]
    fun test_will_oog_and_fail() { other_module::will_oog() }
}

4. #[expected_failure(vector_error, minor_status = <u64_opt>, location = <location>)]

这指定了测试预期会在指定位置失败,并引发向量错误,并带有给定的 minor_status(如果提供)。<location> 必须是模块位置的有效路径,例如 Selfmy_package::my_module<u64_opt> 是一个可选参数,指定向量错误的次要状态。如果未指定,则测试失败时,只要引发任何次要状态的向量错误,测试就会通过。如果指定了,则只有在测试失败并引发指定次要状态的向量错误时,测试才会通过。

module pkg_addr::other_module {
    public fun vector_borrow_empty() {
        &vector<u64>[][1];
    }
}

module pkg_addr::my_module {
    #[test]
    #[expected_failure(vector_error, location = Self)]
    fun vector_abort_same_module() {
        vector::borrow(&vector<u64>[], 1);
    }

    #[test]
    #[expected_failure(vector_error, location = pkg_addr::other_module)]
    fun vector_abort_same_module() {
        other_module::vector_borrow_empty();
    }

    // Can specify minor statues (i.e., vector-specific error codes) to expect.
    #[test]
    #[expected_failure(vector_error, minor_status = 1, location = Self)]
    fun native_abort_good_right_code() {
        vector::borrow(&vector<u64>[], 1);
    }

    // FAIL: correct error, but wrong location.
    #[test]
    #[expected_failure(vector_error, location = pkg_addr::other_module)]
    fun vector_abort_same_module() {
        other_module::vector_borrow_empty();
    }

    // FAIL: correct error and location but the minor status differs so this test will fail.
    #[test]
    #[expected_failure(vector_error, minor_status = 0, location = Self)]
    fun vector_abort_wrong_minor_code() {
        vector::borrow(&vector<u64>[], 1);
    }
}

5. #[expected_failure]

这个注解表示,如果测试因_任何_错误代码中止,测试将通过。使用此注解来标注预期失败的测试时应当极其谨慎,并始终优先使用上述描述的方式。以下是一些此类注解的示例:

#[test]
#[expected_failure]
fun test_will_abort_and_pass1() { abort 1 }

#[test]
#[expected_failure]
fun test_will_arith_error_and_pass2() { 1/0; }

仅测试注解

模块及其成员可以声明为仅用于测试。如果一个项目被注解为 #[test_only],则该项目仅在测试模式下编译时才会包含在已编译的 Move 字节码中。此外,在非测试模式下编译时,任何非测试 use#[test_only] 模块将在编译期间引发错误。

注意: 注解为 #[test_only] 的函数仅可从测试代码中调用,但它们本身不是测试,且不会由单元测试框架运行。

#[test_only] // 仅测试属性可以附加到模块
module abc { ... }

#[test_only] // 仅测试属性可以附加到常量
const MY_ADDR: address = @0x1;

#[test_only] // 仅测试属性可以附加到 use
use pkg_addr::some_other_module;

#[test_only] // 仅测试属性可以附加到结构体
public struct SomeStruct { ... }

#[test_only] // 仅测试属性可以附加到函数。只能从测试代码中调用,但这不是一个测试!
fun test_only_function(...) { ... }

运行单元测试

Move 包的单元测试可以使用 sui move test 命令运行。

在运行测试时,每个测试将 PASSFAILTIMEOUT。如果测试用例失败,如果可能,将报告失败的位置和引起失败的函数名称。可以在下面的示例中看到这一点。

如果测试用例超出了可以为任何单个测试执行的最大指令数,则该测试将被标记为超时。可以使用以下选项更改此界限。此外,虽然测试结果始终是确定性的,但默认情况下测试是并行运行的,因此除非使用单线程运行,否则测试结果的顺序是非确定性的,这可以通过一个选项进行配置。

上述选项是许多选项中的两个,可以微调测试并帮助调试失败的测试。要查看所有可用选项及其描述,请将 --help 标志传递给 sui move test 命令:

$ sui move test --help

示例

以下示例展示了一个使用一些单元测试功能的简单模块:

首先创建一个空包并切换到该目录:

$ sui move new test_example; cd test_example

然后在 sources 目录下添加以下模块:

// 文件名:sources/my_module.move
module test_example::my_module {

    public struct Wrapper(u64)

    const ECoinIsZero: u64 = 0;

    public fun make_sure_non_zero_coin(coin: Wrapper): Wrapper {
        assert!(coin.0 > 0, ECoinIsZero);
        coin
    }

    #[test]
    fun make_sure_non_zero_coin_passes() {
        let coin = Wrapper(1);
        let Wrapper(_) = make_sure_non_zero_coin(coin);
    }

    #[test]
    // 或者 #[expected_failure] 如果我们不关心中止代码
    #[expected_failure(abort_code = ECoinIsZero)]
    fun make_sure_zero_coin_fails() {
        let coin = Wrapper(0);
        let Wrapper(_) = make_sure_non_zero_coin(coin);
    }

    #[test_only] // 仅用于测试的辅助函数
    fun make_coin_zero(coin: &mut Wrapper) {
        coin.0 = 0;
    }

    #[test]
    #[expected_failure(abort_code = ECoinIsZero)]
    fun make_sure_zero_coin_fails2() {
        let mut coin = Wrapper(10);
        make_coin_zero(&mut coin);
        let Wrapper(_) = make_sure_non_zero_coin(coin);
    }
}

运行测试

你可以使用 move test 命令运行这些测试:

$ sui move test
INCLUDING DEPENDENCY Sui
INCLUDING DEPENDENCY MoveStdlib
BUILDING test_example
Running Move unit tests
[ PASS    ] 0x0::my_module::make_sure_non_zero_coin_passes
[ PASS    ] 0x0::my_module::make_sure_zero_coin_fails
[ PASS    ] 0x0::my_module::make_sure_zero_coin_fails2
Test result: OK. Total tests: 3; passed: 3; failed: 0

使用测试标志

运行特定测试

你可以使用 sui move test <str> 运行特定测试或一组测试。这将只运行名称中包含 <str> 的测试。例如,如果我们只想运行名称中包含 "non_zero" 的测试:

$ sui move test non_zero
INCLUDING DEPENDENCY Sui
INCLUDING DEPENDENCY MoveStdlib
BUILDING test_example
Running Move unit tests
[ PASS    ] 0x0::my_module::make_sure_non_zero_coin_passes
Test result: OK. Total tests: 1; passed: 1; failed: 0

-i <bound>--gas_used <bound>

这将限定任何一个测试可以消耗的最大气体量为 <bound>

$ sui move test -i 0
INCLUDING DEPENDENCY Sui
INCLUDING DEPENDENCY MoveStdlib
BUILDING test_example
Running Move unit tests
[ TIMEOUT ] 0x0::my_module::make_sure_non_zero_coin_passes
[ FAIL    ] 0x0::my_module::make_sure_zero_coin_fails
[ FAIL    ] 0x0::my_module::make_sure_zero_coin_fails2

Test failures:

Failures in 0x0::my_module:

┌── make_sure_non_zero_coin_passes ──────
│ Test timed out
└──────────────────


┌── make_sure_zero_coin_fails ──────
│ error[E11001]: test failure
│    ┌─ ./sources/my_module.move:22:27
│    │
│ 21 │     fun make_sure_zero_coin_fails() {
│    │         ------------------------- In this function in 0x0::my_module
│ 22 │         let coin = MyCoin(0);
│    │                           ^ Test did not error as expected. Expected test to abort with code 0 <SNIP>
│
│
└──────────────────


┌── make_sure_zero_coin_fails2 ──────
│ error[E11001]: test failure
│    ┌─ ./sources/my_module.move:34:31
│    │
│ 33 │     fun make_sure_zero_coin_fails2() {
│    │         -------------------------- In this function in 0x0::my_module
│ 34 │         let mut coin = MyCoin(10);
│    │                               ^^ Test did not error as expected. Expected test to abort with code 0 <SNIP>
│
│
└──────────────────

Test result: FAILED. Total tests: 3; passed: 0; failed: 3

-s or --statistics

使用这些标志,您可以收集运行的测试的统计信息,并报告每个测试的运行时间和气体使用量。您还可以添加 csv (sui move test -s csv) 以获取气体使用量的 CSV 输出格式。例如,如果我们想查看上述示例中测试的统计信息:

$ sui move test -s
INCLUDING DEPENDENCY Sui
INCLUDING DEPENDENCY MoveStdlib
BUILDING test_example
Running Move unit tests
[ PASS    ] 0x0::my_module::make_sure_non_zero_coin_passes
[ PASS    ] 0x0::my_module::make_sure_zero_coin_fails
[ PASS    ] 0x0::my_module::make_sure_zero_coin_fails2

Test Statistics:

┌────────────────────────────────────────────────┬────────────┬───────────────────────────┐
│                   Test Name                    │    Time    │         Gas Used          │
├────────────────────────────────────────────────┼────────────┼───────────────────────────┤
│ 0x0::my_module::make_sure_non_zero_coin_passes │   0.001    │             1             │
├────────────────────────────────────────────────┼────────────┼───────────────────────────┤
│ 0x0::my_module::make_sure_zero_coin_fails      │   0.001    │             1             │
├────────────────────────────────────────────────┼────────────┼───────────────────────────┤
│ 0x0::my_module::make_sure_zero_coin_fails2     │   0.001    │             1             │
└────────────────────────────────────────────────┴────────────┴───────────────────────────┘

Test result: OK. Total tests: 3; passed: 3; failed: 0

See Sui's Coding Conventions for Move