Move语言入门教程Part2:所有权、能力和数据类型详解

Move语言入门教程Part2:所有权、能力和数据类型详解
SoniaChenMove语言入门教程Part2:所有权、能力和数据类型详解
简介
本教程是Move语言入门系列的第二部分,重点介绍Move的核心概念和数据类型。Move作为区块链安全的编程语言,通过独特的所有权系统和能力机制确保资产安全。我们将详细讲解所有权规则、四大能力(copy、drop、store、key)、各种数据类型的使用方法,以及Move的控制流和表达式特性。
命名规则
别名必须遵循与模块成员同样的命名规则:对结构体(与常量)的别名必须以大写 A…Z 开头。
所有权
copy
任何具有 copy 能力(ability)的值都可以这样拷贝;若未显式写 move,编译器通常会隐式地执行拷贝。
let x = 0;
let y = copy x + 1;
let z = copy x + 2;
move
局部变量
move 则取走(移动)局部变量中的值,而不复制数据。发生 move 之后,即使该值的类型具备 copy 能力,该局部变量本身也不再可用,此时所有权已经被转移,因此该局部变量已经失效。
let x = 1;
let y = move x + 1;
// ------ 此处发生了 move
let z = move x + 2; // 错误!
// ^^^^^^ 局部变量 `x` 已被移动,无法再用
y + z
函数返回和入参
如果函数返回局部变量a,那么a的所有权就转移给调用者
module book::ownership;
public fun owner(): u8 {
let a = 1; // a 在此定义
a // 作用域结束,a 被返回
}
#[test]
fun test_owner() {
let a = owner();
// 此处 a 有效
} // a 在此被丢弃
如果变量a被传值到另外的函数,所有权也会被转移
module book::ownership;
public fun owner(): u8 {
let a = 10;
a
} // a 被返回
public fun take_ownership(v: u8) {
// v 归 `take_ownership` 所有
} // v 在此被丢弃
#[test]
fun test_owner() {
let a = owner();
// `u8` 是可复制类型;调用时传 `move a` 可显式转移其所有权
take_ownership(move a);
// 此处 a 已无效
}
推断
未指明 copy / move,编译器会进行推断。规则很简单:
l 具有 copy 能力的值 → 推断为 copy;
l 所有引用(不可变 & 与可变 &mut)→ 推断为 copy;
n 但在某些特殊情形下,为避免可预期的借用检查错误,编译器会把该处改判为 move(发生在引用不再被使用之后)。
其他一切值 → 推断为 move。
能力
copy
允许具有该能力的类型的值被拷贝(copy)。
public struct Copyable has copy {}
let a = Copyable {}; // 允许:因为 Copyable 具有 `copy`
let b = a; // 隐式拷贝:把 a 拷贝到 b
let c = *&b; // 显式拷贝:通过解引用获得一份拷贝
// Copyable 不具有 `drop`,因此每个实例(a、b、c)都必须被使用
// 或显式析构;下面用解构来“消耗”它们(防止未使用报错)
let Copyable {} = a;
let Copyable {} = b;
let Copyable {} = c;
在 Move 中,使用“空花括号的解构”常被用来消费未使用变量,尤其是对不具备 drop 的类型,以避免值在离开作用域时未被显式使用而触发编译错误。同时,Move 在解构时要求写出类型名(如 let Copyable {} = a;),以强化严格的类型与所有权规则。
所有原生类型(native types)都具备 copy,包括:
- bool
- 无符号整数
- vector
- address
标准库(std)中的类型也具备 copy,包括:
- Option
- String
- TypeName
drop
允许具有该能力的类型的值被丢弃/弹出(pop/drop)。
没有drop的话,如果你后续不使用这个结构的变量 ,后续就会报错。所以需要进行解包。
let Copyable {} = a;
---
比如事件就需要
event::emit(BoxPurchased{
buyer: tx_context::sender(ctx),
nft_id,
rarity: rarity,
});
store
允许具有该能力的类型的值存在于存储中的某个值内部。
结构体所有的字段都需要具备store能力,然后具有store能力的结构内所有字段也必须要具备store
key
具有 key 能力的 struct 被视为对象,可用于各类存储函数。
Sui Verifier 要求该 struct 的第一个字段必须命名为 id,类型为 UID。此外,它还要求所有字段都具备 store 能力。
// `User` 对象定义。
public struct User has key {
id: UID, // Sui 字节码验证器要求
name: String, // 各字段类型须具备 `store`
}
/// 创建一个新的 `User` 实例。
/// 使用特殊结构体 `TxContext` 派生唯一 ID(UID)。
public fun new(name: String, ctx: &mut TxContext): User {
User {
id: object::new(ctx), // 生成新的 UID
name,
}
}
UID 类型不具备 drop 或 copy。而任何具备 key 的类型都必须含有一个 UID 字段,因此:具有 key 的类型永远不可能拥有 drop 或 copy。这一性质可用于能力约束:
- 若约束要求 drop 或 copy,就自动排除了 key;
- 反之,若要求 key,就排除了具有 drop 或 copy 的类型。
由于 key 类型强制需要 UID 字段,所有原生类型与标准库类型都不可能具备 key。key 只出现在部分 Sui Framework 类型以及自定义类型中。
/// 盲盒系列
public struct Collection has key {
id: UID,
name: String,
price: u64, // 每个盲盒的价格(MIST)
total_supply: u64, // 总供应量
minted: u64, // 已售出数量
balance: Balance<SUI>, // 收益池
}
/// NFT(开盒后获得)
public struct NFT has key, store {
id: UID,
name: String,
rarity: String, // Common/Rare/Legendary
image_url: String,
}
内建类型具有的能力如下所示:
- 所有原始/内建类型都具有 copy、drop、store:
- bool, u8, u16, u32, u64, u128, u256, address → 具备 copy、drop、store。
- vector
是否具备 copy、drop、store 取决于 T 的能力。 - 不可变引用 & 与可变引用 &mut 都具有 copy、drop(指拷贝/丢弃引用自身,而非其指向的值)。
- 引用不能出现在全局存储中,因此它们没有 store。
- 注意:无原始类型具有 key,也就是说它们不能直接参与顶层存储操作。
Struct/Enum
因此,当为 struct 声明能力时,所有字段都必须满足下列要求,以确保可达性(reachability)规则成立:
- l 若声明 copy,则所有字段都必须具有 copy。
- l 若声明 drop,则所有字段都必须具有 drop。
- l 若声明 store,则所有字段都必须具有 store。
- l 若声明 key,则所有字段都必须具有 store(注意 key 本身对字段不要求 key)。
对 enum,除 key 外,其余能力都可声明(enum 不能作为存储顶层值/对象,因此不能有 key)。同样的字段规则适用于枚举的各变体字段:
- l 若枚举声明了 copy,则所有变体的所有字段都必须具有 copy;
- l 声明了 drop → 所有字段都必须有 drop;
- l 声明了 store → 所有字段都必须有 store;
- l key → 不允许用于 enum。
泛型
当对泛型类型注解能力时,并不保证该类型的所有实例都具备这些能力。考虑如下定义:
public struct Cup
我们希望 Cup 能容纳任意类型(不受其能力限制)。类型系统在实例化时可见类型参数,因此当遇到会破坏能力保证的类型实参时,应当“移除”相应能力。
数据类型
Integer
- Move 不支持溢出/下溢
- 数值字面量可以直接写成数字序列(如 112),也可以写成十六进制字面量(如 0xFF)。
- 数字字面量可以使用下划线分组以增强可读性(例如 1_234_5678、1_000u128、0xAB_CD_12_35)。
Address
// address 字面量
let value: address = @0x1;
// 在 Move.toml 中注册过的命名地址
let value = @std;
let other = @sui;
Struct
定义结构体
结构体使用
-
销毁结构体:
Move 的哲学是安全第一:如果你要拆开一个结构体,就默认是想移动(销毁)它,以确保原结构体不再持有任何悬空的数据。
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; // ^ 等价于 `x: x` // 新绑定:x: u64 = 3, foo_y: bool = false }这里因为Foo没有实现copy能力,所以在这个解构的过程中会被move
-
字段读:
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; let y = foo.y;没有或者字段是非原始类型(结构体或者vec),就会报错
let foo = Foo { x: 3, y: true }; let bar = Bar(foo); let foo2: Foo = *&bar.0; // OK let foo3: Foo = bar.0; // 错误:应显式复制 *& -
字段写:
需要可变借用后赋值
let mut foo = Foo { x: 3, y: true }; *&mut foo.x = 42; // Foo { x: 42, y: true } *&mut foo.y = !foo.y; // Foo { x: 42, y: false } let mut bar = Bar(foo); // Bar(Foo { x: 42, y: false }) *&mut bar.0.x = 52; // Bar(Foo { x: 52, y: false }) *&mut bar.0 = Foo { x: 62, y: true }; // Bar(Foo { x: 62, y: true })直接使用点号修改需要字段有drop能力
let mut foo = Foo { x: 3, y: true }; foo.x = 42; foo.y = !foo.y; let mut bar = Bar(foo); bar.0.x = 52; bar.0 = Foo { x: 62, y: true };或者结构体的可变引用
let mut foo = Foo { x: 3, y: true }; let foo_ref = &mut foo; foo_ref.x = foo_ref.x +1;
枚举
枚举(enum)是包含一个或多个变体(variant)的用户自定义数据结构。每个变体可可选地携带带类型的字段;不同变体的字段数量与类型彼此可以不同。枚举中的字段可以存放除“引用”和“元组”以外的任何类型,包括其他结构体或枚举。
与结构体类似,枚举也可以带有能力(abilities)来控制其可执行的操作。但需要注意:枚举不能拥有 key 能力(因为它们不能作为顶层对象存在)。
枚举不允许递归
覆盖枚举值
只要枚举具备 drop 能力,就可以像其他值一样,用同类型的新值覆盖它:
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));
引用
不可变引用与可变引用都可以读取,读取将产生其指向值的一份拷贝。为了允许读取引用,底层类型必须具备 copy 能力,因为读取会创建一份新拷贝。该规则可防止资产被复制。
所有权与复制
不可变引用与可变引用都可以用来复制与拓展,只是已经存在其他副本或拓展
fun reference_copies(s: &mut S) {
let s_copy1 = s; // OK:复制引用
let s_extension = &mut s.f; // OK:在引用上扩展字段引用
let s_copy2 = s; // 仍然 OK
...
}
引用不可存储
和rust不一样,引用和元祖是不能作为结构体字段存储的类型
Option
表示值可能存在也可能不存在的类型
String
Move 没有内建的字符串类型,但标准库提供了两种实现:
- std::string 模块中的 String(UTF-8 字符串);
- std::ascii 模块中的 ASCII 字符串。
在 Sui 执行环境中,交易输入里的字节向量会自动转换为 String。因此,在许多场景下无需在交易块中手动构造 String。
std::string
虽然有 string 与 ascii 两种字符串,默认推荐使用 std::string。它的许多操作由底层原生实现,性能更好;而 std::ascii 完全用 Move 实现,更偏高层抽象,不太适合性能敏感场景。
创建 String
常用操作
let mut str = b"Hello,".to_string();
let another = b" World!".to_string();
// 追加:把另一字符串接到末尾
str.append(another);
// 切片拷贝:sub_string(start, end)
str.sub_string(0, 5); // "Hello"
// 字节长度(注意:是字节数而非“字符数”)
str.length(); // 12
// 链式调用:取子串再看长度
str.sub_string(0, 5).length(); // 5
// 判空
str.is_empty(); // false
// 取底层字节向量做自定义处理
let bytes: &vector<u8> = str.bytes();
强制类型转换
move支持在整数类型之间进行显式类型转换:
let x: u8 = 42;
let y: u16 = x as u16;
let z = 2 * (x as u16); // ambiguous, requires parentheses
一个更复杂的示例,可以用于防止溢出:
let x: u8 = 255;
let y: u8 = 255;
let z: u16 = (x as u16) + ((y as u16) * 2);
泛型
声明类型参数
类型实参
类型参数约束
变量
Move 使用 let 将变量名与值绑定:不能以大写字母开头。局部变量的类型通常可由 Move 的类型系统自动推断。但你也可以显式标注以提升可读性/可调试性。
当类型实参无法被推断时需要注解。最常见的是泛型类型的类型参数无法推断:
let _v1 = vector[]; // ERROR!// ^^^^^^^^ 无法推断元素类型;需要添加注解let v2: vector
解构
元组解构
let 可以通过元组一次引入多个局部变量。括号内的各局部变量会以元组右侧对应位置的值初始化:
常量
常量是在模块内部为共享的静态值起名的一种方式。
常量的取值必须在编译期可知。该值被存入已编译模块中;每次使用常量都会拷贝一份该值。
public 或 public(package) 暂不支持用于常量。const 值仅能在其声明的模块内使用。不过,作为便利,在单元测试属性(unit test attributes)中可以跨模块使用。
声明
const
流程控制
循环
循环允许你为函数体中的某个位置定义标签并转移控制流。例如,我们可以嵌套两个循环,并通过带标签的 break/continue 精确指定控制流。给任意 loop 或 while 前加 'label: 前缀,就可以直接跳转到该处进行 break 或 continue。
fun sum_until_threshold(input: &vector<vector<u64>>, threshold: u64): u64 {
let mut sum = 0;
let mut i = 0;
let input_size = input.length();
'outer: loop {
// 跳出到 outer,因为它是最近的包围循环
if (i >= input_size) break sum;
let vec = &input[i];
let size = vec.length();
let mut j = 0;
while (j < size) {
let v_entry = vec[j];
if (sum + v_entry < threshold) {
sum = sum + v_entry;
} else {
// 下一个元素会使总和越过阈值,
// 因此返回当前 sum
break 'outer sum
};
j = j + 1;
};
i = i + 1;
}
}
也适用于更大的代码中的多层循环
let x = 'outer: loop {
...
'inner: while (cond) {
...
if (cond0) { break 'outer value };
...
if (cond1) { continue 'inner }
else if (cond2) { continue 'outer }
...
}
...
};
表达式
1. 表达式的本质与串联
- Move 的哲学: 在 Move 中,几乎一切都是表达式(
let语句除外,它是声明)。这意味着大多数代码片段都会产生一个值。 - 代码块与分号: 代码块
{}的值是块内 最后一个 表达式的值(不能以分号;结尾)。如果用分号串联表达式,或块内以分号结束,则该表达式/块的值为 单位值()。
2. 算术运算的安全性(Abort 优先)
- 严格类型一致: 算术运算(
+,-,*,%,/)的操作数必须是相同类型,否则需要显式转换(cast)。 - 强制中止(Abort): Move 不会产生未定义或环绕结果。任何不符合“数学意义上的整数”的结果(例如 溢出、下溢、被零除)都会触发 中止(Abort),即交易失败并回滚。这是 Move 安全性的重要体现。
3. 位运算和位移运算的特点
- 位运算(
&,|,^): 纯粹的比特序列操作,不会触发中止。 - 位移运算(
<<,>>): 右操作数(位移位数)必须是u8。当位移位数 大于等于 该整数类型的位宽时,会触发 中止(例如u64位移 ≥ 64 位时中止)。
4. 相等性(== / !=)的核心约束
- 类型严格一致: 相等运算要求两侧操作数的 类型必须完全相同。
- 资源的“销毁”语义: 直接对 值 进行相等比较(例如
c1 == c2,而不是引用)会消耗掉这两个值(类似于被 销毁)。因此,被比较的类型必须具备drop能力,否则会报错。 - 引用比较的宽松性: 在比较引用时,引用的 可变性(
&或&mut)不影响比较结果,行为等价于对&mut显式应用freeze(前提是底层类型一致)。 - 自动借用(Move 2024+): 当一侧是引用而另一侧不是时,编译器会自动将非引用一侧 不可变借用(
&) 后再进行比较。
5. 地址字面量和数值推断
- 地址字面量(
@): 在表达式中使用地址时,必须以@开头(例如@0x1,@std)。在非表达式上下文(如模块声明)中则不用@。 - 数值类型推断: 未显式标注类型后缀的数字字面量,编译器会尝试从上下文推断;如果无法推断,默认视为
u64。
6. 避免不必要的拷贝(性能优化)
- 最佳实践: 尽管拥有
drop能力的值可以直接比较,但对于大型数据结构(如vector<u8>或大型struct),为了避免昂贵的copy操作,强烈建议使用引用(&)进行比较。
函数
若要允许从其他模块访问,需将函数声明为 public 或 public(package)。与可见性相关的,还有 entry 修饰符:它决定函数是否能作为执行入口被调用。
entry 修饰符
除了 public 函数外,有些函数希望作为执行入口触发。entry 允许模块函数作为程序的起始点,而无需对其他模块公开。
本质上,public 与 entry 的组合定义了模块的“主函数”(可作为程序起点)。注意:entry 函数依旧可以被其他 Move 函数调用,它并不只限于“起点”用途。
macro 修饰符
宏函数不同于普通函数:它们在编译期于调用点内联展开,不存在于运行时。
macro fun n_times($n: u64, $body: |u64| -> ()) {
let n = $n;
let mut i = 0;
while (i < n) {
$body(i);
i = i + 1;
}
}
fun example() {
let mut sum = 0;
n_times!(10, |x| sum = sum + x );
}
方法函数
方法函数



