Skip to content
🎉 Welcome to the new Aptos Docs! Click here to submit an issue.
构建Smart Contracts (Move)Move Book | Move 书籍Structs and Resources | 结构体与资源

结构体与资源

结构体 是一种包含类型化字段的用户自定义数据结构。结构体可以存储任何非引用类型,包括其他结构体。

如果结构体值不能被复制且不能被丢弃,我们通常将其称为 资源 。 在这种情况下,资源值必须在函数结束时转移所有权。这一特性使得资源特别适合用于定义全局存储模式或表示重要值(例如代币)。

默认情况下,结构体是线性且短暂的。 这意味着它们:不能被复制、不能被丢弃,也不能存储在全局存储中。这表示所有值都必须转移所有权(线性), 并且这些值必须在程序执行结束时被处理(短暂)。 我们可以通过为结构体添加 能力 来放宽这一限制,这些能力允许值被复制或丢弃, 也可以存储在全局存储中或用于定义全局存储模式。

定义结构体

结构体必须在模块内定义:

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 }
}

更多细节请参阅 结构体注解 部分。

命名规范

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

module 0x2::m {
  struct Foo {}
  struct BAR {}
  struct B_a_z_4_2 {}
}

这种以 AZ 开头的命名限制是为了给未来语言特性留出空间。 未来可能会也可能不会取消此限制。

使用结构体

创建结构体

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

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() };
  }
}

注意结构体没有可见性修饰符(如 publicprivate)。

所有权

如前文定义结构体所述,结构体默认是线性且临时的。这意味着它们不能被复制或丢弃。 该特性在建模现实资源(如货币)时非常有用,因为你不希望货币被复制或在流通中丢失。

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;
  }
}

请注意,你只能在定义资源的模块中解构资源。这一特性可以用来强制系统保持某些不变量,例如货币守恒。

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

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起

结构体可以声明为具有 位置字段 ,这些字段没有名称只有编号。 位置结构体的行为与常规结构体类似,只是提供了不同的语法,可能更适合字段较少的使用场景。

位置结构体的字段按出现顺序分配。在下面的示例中,字段 0u64 类型, 字段 1u8 类型:

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.0value.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 ) 或更经典的数据(如 PointCircle)。

示例 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
  }
}