结构体与资源
结构体 是一种包含类型化字段的用户自定义数据结构。结构体可以存储任何非引用类型,包括其他结构体。
如果结构体值不能被复制且不能被丢弃,我们通常将其称为 资源 。 在这种情况下,资源值必须在函数结束时转移所有权。这一特性使得资源特别适合用于定义全局存储模式或表示重要值(例如代币)。
默认情况下,结构体是线性且短暂的。 这意味着它们:不能被复制、不能被丢弃,也不能存储在全局存储中。这表示所有值都必须转移所有权(线性), 并且这些值必须在程序执行结束时被处理(短暂)。 我们可以通过为结构体添加 能力 来放宽这一限制,这些能力允许值被复制或丢弃, 也可以存储在全局存储中或用于定义全局存储模式。
定义结构体
结构体必须在模块内定义:
module 0x2::m {
struct Foo { x: u64, y: bool }
struct Bar {}
struct Baz { foo: Foo }
// ^ 注意:允许末尾有逗号
}
结构体不能递归定义,因此以下定义是无效的:
module 0x2::m {
struct Foo { x: Foo }
// ^ 错误!Foo不能包含Foo
}
关于使用编号字段而非命名字段的位置结构体,请参阅 位置结构体 部分。
如上所述:默认情况下,结构体声明是线性且短暂的。因此,为了允许值进行某些操作(如复制、丢弃、存储到全局存储或用作存储模式),
可以通过用 has <能力>
标注来为结构体授予 能力 :
module 0x2::m {
struct Foo has copy, drop { x: u64, y: bool }
}
更多细节请参阅 结构体注解 部分。
命名规范
结构体名称必须以大写字母 A
到 Z
开头。首字母之后,
结构体名称可以包含下划线 _
、字母 a
到 z
、字母 A
到 Z
或数字 0
到 9
。
module 0x2::m {
struct Foo {}
struct BAR {}
struct B_a_z_4_2 {}
}
这种以 A
到 Z
开头的命名限制是为了给未来语言特性留出空间。
未来可能会也可能不会取消此限制。
使用结构体
创建结构体
可以通过指定结构体名称后跟每个字段的值来创建(或”打包”)结构体类型的值:
module 0x2::m {
struct Foo has drop { x: u64, y: bool }
struct Baz has drop { foo: Foo }
fun example() {
let foo = Foo { x: 0, y: false };
let baz = Baz { foo };
}
}
如果使用与字段同名的局部变量初始化结构体字段,可以使用以下简写形式:
module 0x2::m {
fun example() {
let baz = Baz { foo: foo };
// 等价于
let baz = Baz { foo };
}
}
这种情况有时被称为”字段名双关 “field name punning”。
通过模式匹配解构结构体
可以通过模式绑定或赋值来解构结构体值。
module 0x2::m {
struct Foo { x: u64, y: bool }
struct Bar { foo: Foo }
struct Baz {}
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
}
fun example_destroy_foo_wildcard() {
let foo = Foo { x: 3, y: false };
let Foo { x, y: _ } = foo;
// 由于 y 被绑定到通配符,只创建一个新绑定
// x: u64 = 3
}
fun example_destroy_foo_assignment() {
let x: u64;
let y: bool;
Foo { x, y } = Foo { x: 3, y: false };
// 修改现有变量x和y
// x = 3, y = false
}
fun example_foo_ref() {
let foo = Foo { x: 3, y: false };
let Foo { x, y } = &foo;
// 创建两个新绑定
// x: &u64
// y: &bool
}
fun example_foo_ref_mut() {
let foo = Foo { x: 3, y: false };
let Foo { x, y } = &mut foo;
// 创建两个新绑定
// x: &mut u64
// y: &mut bool
}
fun example_destroy_bar() {
let bar = Bar { foo: Foo { x: 3, y: false } };
let Bar { foo: Foo { x, y } } = bar;
// ^ 嵌套模式
// 创建两个新绑定
// x: u64 = 3
// y: bool = false
}
fun example_destroy_baz() {
let baz = Baz {};
let Baz {} = baz;
}
}
借用结构体及其字段
可以使用 &
和 &mut
运算符创建对结构体或字段的引用。
以下示例包含一些可选类型标注(如 : &Foo
)来演示操作类型。
module 0x2::m {
fun example() {
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; // 通过可变引用修改字段
}
}
可以借用嵌套结构体的内部字段:
module 0x2::m {
fun example() {
let foo = Foo { x: 3, y: true };
let bar = Bar { foo };
let x_ref = &bar.foo.x;
}
}
也可以通过结构体引用来借用字段:
module 0x2::m {
fun example() {
let foo = Foo { x: 3, y: true };
let foo_ref = &foo;
let x_ref = &foo_ref.x;
// 效果等同于 let x_ref = &foo.x
}
}
读取和写入字段
如果字段是可复制的,可以通过解引用借用的字段来读取和复制字段值:
module 0x2::m {
fun example() {
let foo = Foo { x: 3, y: true };
let bar = Bar { foo: copy foo };
let x: u64 = *&foo.x;
let y: bool = *&foo.y;
let foo2: Foo = *&bar.foo;
}
}
点运算符可用于读取和复制结构体的任何可复制字段,无需显式借用和解引用:
module 0x2::m {
fun example() {
let foo = Foo { x: 3, y: true };
let x = foo.x; // x == 3
let y = foo.y; // y == true
let bar = Bar { foo };
let foo2: Foo = *&bar.foo; // `Foo` 必须可复制
let foo3: Foo = bar.foo; // 同上
}
}
点运算符可以链式使用以访问嵌套字段:
module 0x2::m {
fun example() {
let baz = Baz { foo: Foo { x: 3, y: true } };
let x = baz.foo.x; // x = 3;
}
}
此外,点语法还可用于修改字段。
module 0x2::m {
fun example() {
let 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 bar = Bar { foo }; // bar = Bar { foo: Foo { x: 42, y: false } }
bar.foo.x = 52; // bar = Bar { foo: Foo { x: 52, y: false } }
bar.foo = Foo { x: 62, y: true }; // bar = Bar { foo: Foo { x: 62, y: true } }
}
}
点语法同样适用于结构体的引用:
module 0x2::m {
fun example() {
let foo = Foo { x: 3, y: true };
let foo_ref = &mut foo;
foo_ref.x = foo_ref.x + 1;
}
}
特权结构体操作
大多数针对结构体类型 T
的操作只能在声明 T
的模块内执行:
- 结构体类型只能在定义该结构体的模块内被创建(“打包”)或销毁(“解包”)
- 结构体的字段只能在定义该结构体的模块内访问
遵循这些规则,若需要在模块外修改结构体,必须为其提供公共 API。本章末尾将提供相关示例。
但结构体 类型 对其他模块或脚本始终可见:
// m.move
module 0x2::m {
struct Foo has drop { x: u64 }
public fun new_foo(): Foo {
Foo { x: 42 }
}
}
// n.move
module 0x2::n {
use 0x2::m;
struct Wrapper has drop {
foo: m::Foo
}
fun f1(foo: m::Foo) {
let x = foo.x;
// ^ 错误!此处无法访问 `foo` 的字段
}
fun f2() {
let foo_wrapper = Wrapper { foo: m::new_foo() };
}
}
注意结构体没有可见性修饰符(如 public
或 private
)。
所有权
如前文定义结构体所述,结构体默认是线性且临时的。这意味着它们不能被复制或丢弃。 该特性在建模现实资源(如货币)时非常有用,因为你不希望货币被复制或在流通中丢失。
module 0x2::m {
struct Foo { x: u64 }
public fun copying_resource() {
let foo = Foo { x: 100 };
let foo_copy = copy foo; // 错误!'copy' 操作需要 'copy' 能力
let foo_ref = &foo;
let another_copy = *foo_ref // 错误!解引用需要 'copy' 能力
}
public fun destroying_resource1() {
let foo = Foo { x: 100 };
// 错误!函数返回时 foo 仍包含值
// 此销毁操作需要 'drop' 能力
}
public fun destroying_resource2(f: &mut Foo) {
*f = Foo { x: 100 } // 错误!
// 通过写入销毁旧值需要 'drop' 能力
}
}
要修复第二个示例( fun destroying_resource1
),你需要手动 “解包” 资源:
module 0x2::m {
struct Foo { x: u64 }
public fun destroying_resource1_fixed() {
let foo = Foo { x: 100 };
let Foo { x: _ } = foo;
}
}
请注意,你只能在定义资源的模块中解构资源。这一特性可以用来强制系统保持某些不变量,例如货币守恒。
另一方面,如果你的结构体不代表有价值的事物,可以添加 copy
和 drop
能力来获得一个可能更符合其他编程语言习惯的结构体值:
module 0x2::m {
struct Foo has copy, drop { x: u64 }
public fun run() {
let foo = Foo { x: 100 };
let foo_copy = copy foo;
// ^ 这行代码复制了 foo,而 `let x = foo` 或
// ` let x = move foo` 都会移动 foo
let x = foo.x; // x = 100
let x_copy = foo_copy.x; // x = 100
// 当函数返回时, foo 和 foo_copy 都会被隐式丢弃
}
}
位置结构体
自语言版本2.0起
结构体可以声明为具有 位置字段 ,这些字段没有名称只有编号。 位置结构体的行为与常规结构体类似,只是提供了不同的语法,可能更适合字段较少的使用场景。
位置结构体的字段按出现顺序分配。在下面的示例中,字段 0
是 u64
类型,
字段 1
是 u8
类型:
module 0x2::m {
struct Pair(u64, u8);
}
位置结构体的能力声明在字段列表 之后 而非之前:
module 0x2::m {
struct Pair(u64, u8) has copy, drop;
}
对于纯类型标签( Move 代码中常用于 phantom 类型),也可以完全省略参数列表:
module 0x2::m {
struct TypeTag has copy, drop;
}
位置结构体的值创建和解构如下所示:
使用 PositionalStructs(参数)
语法:
module 0x2::m {
fun work() {
let value = Pair(1, true);
let Pair(number, boolean) = value;
assert!(number == 1 && boolean == true);
}
}
位置结构体的字段可以使用位置作为字段选择器来访问。例如在上面的代码中,
可以使用 value.0
和 value.1
来访问两个字段而无需解构 value
。
部分模式
自语言版本2.0起
模式可以使用 ..
表示法来匹配结构体或具名字段变体中未列出的剩余字段,
以及位置字段结构体或变体中开头或结尾被省略的字段。以下是一些示例:
module 0x2::m {
struct Foo{ x: u8, y: u16, z: u32 }
struct Bar(u8, u16, u32);
fun foo_get_x(self: &Foo): u16 {
let Foo{y, ..} = self;
x
}
fun bar_get_0(self: &Foo): u8 {
let Bar(x, ..) = self;
x
}
fun bar_get_2(self: &Foo): u52 {
// 对于位置结构体,也可以将..
// 放在开头
let Bar(.., z) = self;
z
}
}
请注意,目前部分模式 ( partial patterns ) 不能用作赋值的左侧。
虽然可以使用 let Bar(x, ..) = v
,但我们暂不支持 let x; Bar(x, ..) = v
这种写法。
在全局存储中存储资源
具有 key
能力的结构体可以直接保存到 持久化全局存储中。
存储在这些 key
结构体内部的所有值都必须具备 store
能力。详见 能力 和 全局存储 章节。
示例
以下是两个简短的示例,展示如何使用结构体表示有价值的数据(如 Coin
)
或更经典的数据(如 Point
和 Circle
)。
示例 1 :代币
module 0x2::m {
// 我们不希望代币被复制,因为那会导致"货币"重复,
// 因此不给该结构体赋予 'copy' 能力。
// 同样地,我们不希望程序员销毁代币,因此不给结构体赋予 'drop' 能力。
// 但我们*希望*模块用户能将此代币存入持久化全局存储,
// 所以赋予结构体 'store' 能力。该结构体只会存在于全局存储的其他资源中,
// 因此不赋予 'key' 能力。
struct Coin has store {
value: u64,
}
public fun mint(value: u64): Coin {
// 需要对此函数设置某种访问控制,防止模块用户无限铸币
Coin { value }
}
public fun withdraw(coin: &mut Coin, amount: u64): Coin {
assert!(coin.balance >= amount, 1000);
coin.value = coin.value - amount;
Coin { value: amount }
}
public fun deposit(coin: &mut Coin, other: Coin) {
let Coin { value } = other;
coin.value = coin.value + value;
}
public fun split(coin: Coin, amount: u64): (Coin, Coin) {
let other = withdraw(&mut coin, amount);
(coin, other)
}
public fun merge(coin1: Coin, coin2: Coin): Coin {
deposit(&mut coin1, coin2);
coin1
}
public fun destroy_zero(coin: Coin) {
let Coin { value } = coin;
assert!(value == 0, 1001);
}
}
示例 2 : 几何图形
module 0x2::point {
struct Point has copy, drop, store {
x: u64,
y: u64,
}
public fun new(x: u64, y: u64): Point {
Point {
x, y
}
}
public fun x(p: &Point): u64 {
p.x
}
public fun y(p: &Point): u64 {
p.y
}
fun abs_sub(a: u64, b: u64): u64 {
if (a < b) {
b - a
}
else {
a - b
}
}
public fun dist_squared(p1: &Point, p2: &Point): u64 {
let dx = abs_sub(p1.x, p2.x);
let dy = abs_sub(p1.y, p2.y);
dx*dx + dy*dy
}
}
module 0x2::circle {
use 0x2::point::{Self, Point};
struct Circle has copy, drop, store {
center: Point,
radius: u64,
}
public fun new(center: Point, radius: u64): Circle {
Circle { center, radius }
}
public fun overlaps(c1: &Circle, c2: &Circle): bool {
let dist_squared_value = point::dist_squared(&c1.center, &c2.center);
let r1 = c1.radius;
let r2 = c2.radius;
dist_squared_value <= r1*r1 + 2*r1*r2 + r2*r2
}
}