ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Aptos Move 结构体、资源与枚举完全指南:从定义、能力到可见性与模式匹配

Aptos Move 结构体、资源与枚举完全指南:从定义、能力到可见性与模式匹配 Aptos Move 结构体、资源与枚举完全指南从定义、能力到可见性与模式匹配【免费下载链接】aptos-coreAptos is a layer 1 blockchain built to support the widespread use of blockchain through better technology and user experience.项目地址: https://gitcode.com/GitHub_Trending/ap/aptos-core导读本文以 Move 语言手册《Structs, Resources, and Enums》章节为骨架系统讲解 Aptos Move 中用户自定义类型的三大家族结构体struct、资源resource与枚举enum。你将掌握如何在模块中定义与使用结构体、如何通过 abilities能力控制类型的复制、丢弃与全局存储行为、如何用资源模型保护链上资产如 Coin、以及 Move 2.0/2.4 引入的位置结构体、match 模式匹配、跨模块可见性修饰符等进阶特性。读完本文你能够读懂并编写 Aptos 框架中真实的类型设计代码如 coin.move 中的 Coin/CoinStore/CoinInfo并为自己的智能合约选择正确的类型建模方案。一、结构体Struct与资源Resource1.1 核心概念什么是结构体与资源结构体struct是用户自定义的、包含类型化字段typed fields的数据结构。结构体可以存储任何非引用类型包括其他结构体本身。在 Move 中资源resource并不是独立的语法关键字而是对具有特定行为的结构体的称谓当结构体的值**既不能被复制copy也不能被丢弃drop**时我们就称之为资源。这样的资源值必须在函数结束时完成所有权转移这一特性使资源非常适合用于定义全局存储global storage的数据模式表示重要数值例如代币 token。默认的线性 瞬态模型默认情况下结构体是linear线性且ephemeral瞬态的含义是不能被复制cannot be copied不能被丢弃cannot be dropped不能被存储到全局存储cannot be stored in global storage。这意味着所有值都必须转移所有权线性并且必须在程序执行结束前被处理完毕瞬态。通过为结构体赋予 abilities 能力可以放宽这些约束允许值被复制、被丢弃、被存入全局存储或作为全局存储的数据模式。1.2 定义结构体结构体必须定义在模块内部module 0x2::m { struct Foo { x: u64, y: bool } struct Bar {} struct Baz { foo: Foo, } // ^ 注意允许尾随逗号 }结构体不能递归以下定义是非法的module 0x2::m { struct Foo { x: Foo } // ^ error! Foo 不能包含 Foo }对于使用编号字段而非命名字段的位置结构体positional structs见下文位置结构体一节。默认情况下结构体声明是线性且瞬态的。要让值能配合某些操作复制、丢弃、存入全局存储或用作存储模式可以通过has ability注解为结构体授予能力module 0x2::m { struct Foo has copy, drop { x: u64, y: bool } }1.3 命名规则结构体名称必须以大写字母A到Z开头首字母之后可以包含下划线_、小写字母a到z、大写字母A到Z以及数字0到9module 0x2::m { struct Foo {} struct BAR {} struct B_a_z_4_2 {} }这条必须以大写字母开头的限制是为了给未来的语言特性留出空间未来版本可能会移除或保留它。1.4 使用结构体创建打包结构体通过写出结构体名称并跟每个字段的值来创建或称打包packing结构体值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 }; } }如果字段名与局部变量名相同可以使用字段名简写field name punningmodule 0x2::m { fun example() { let baz Baz { foo: foo }; // 等价于 let baz Baz { foo }; } }通过模式匹配销毁结构体结构体值可以通过在let绑定或赋值中做模式匹配来销毁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; // 只有 x 一个绑定y 被通配符 _ 忽略 } 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 } }读写字段如果字段可复制可以通过解引用dereference被借用的字段来读取并复制字段值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; } }点运算符dot operator可以无需显式借用与解引用直接读取并复制结构体的任意可复制字段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; } }1.5 特权结构体操作Privileged Struct Operations对结构体类型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; // ^ error! 这里无法访问 foo 的字段 } fun f2() { let foo_wrapper Wrapper { foo: m::new_foo() }; } }默认情况下结构体没有可见性修饰符——所有结构体操作都限制在定义模块内。Move 2.4 引入了显式可见性修饰符见下文结构体可见性一节。1.6 所有权Ownership资源保护的本质如前所述结构体默认是线性且瞬态的不能被复制或丢弃。这一特性在建模真实世界的资源如货币时非常有用——你显然不希望钱被复制或在流通中丢失module 0x2::m { struct Foo { x: u64 } public fun copying_resource() { let foo Foo { x: 100 }; let foo_copy copy foo; // error! copy-ing 需要 copy 能力 let foo_ref foo; let another_copy *foo_ref // error! 解引用需要 copy 能力 } public fun destroying_resource1() { let foo Foo { x: 100 }; // error! 函数返回时 foo 仍持有值 // 这种销毁需要 drop 能力 } public fun destroying_resource2(f: mut Foo) { *f Foo { x: 100 } // error! // 通过写入销毁旧值需要 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; } }回忆一下你只能在资源定义所在的模块内解构资源。这一限制可以被用来在系统中强制某些不变量例如货币守恒conservation of money。另一方面如果你的结构体并不代表什么宝贵的东西可以通过添加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 在函数返回时都被隐式丢弃 } }1.7 将资源存储到全局存储具有key能力的结构体可以直接保存到持久化全局存储中。所有存储在那些key结构体内部的值都必须具有store能力。更详细的说明见能力与全局存储章节。1.8 实战示例Coin 与几何类型示例 1Coin代币module 0x2::m { // 我们不希望 Coin 被复制因为那会复制这份钱 // 所以不给结构体 copy 能力。 // 同理我们不希望程序员销毁 Coin所以不给 drop 能力。 // 但我们*希望*模块用户可以把这个 coin 存入持久化全局存储 // 所以授予 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.value 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); } }仓库印证Aptos 框架中的真实实现正是采用这一建模思路。在 coin.move 中可以看到struct Coinphantom CoinType has store可转移、可存储但不可复制、不可丢弃的代币本身struct CoinStorephantom CoinType has key作为全局存储根账户持有的代币余额所在处struct CoinInfophantom CoinType has key存储代币的元信息名称、符号、精度等。 其中phantom类型参数只用于类型区分不参与存储布局这是泛型与能力结合的典型用法。同时account.move 中的struct Account has key, store也印证了key类型作为全局存储根的设计模式。示例 2几何类型Point 与 Circlemodule 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 } }注意Circle中嵌套了另一个模块定义的Point结构体作为字段类型——这印证了前面结构体类型对外可见、字段操作仅限定义模块的规则circle模块可以持有Point值并通过point模块公开的 getterx、y、dist_squared来操作它。二、位置结构体Positional Structs自语言版本 2.0 起结构体可以声明为拥有_位置字段_positional fields即字段不是命名而是编号的。位置结构体的行为与普通结构体类似只是提供了一种不同的语法可能更适合字段较少的场景。位置结构体的字段按照出现顺序赋值。下面的示例中字段0的类型是u64字段1的类型是u8module 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(arguments)语法创建和解构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的情况下访问两个字段。三、部分模式Partial Patterns自语言版本 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 } }注意部分模式目前不能用作赋值的左侧。虽然可以使用let Bar(x, ..) v但目前还不支持let x; Bar(x, ..) v。四、枚举Enums自语言版本 2.0 起枚举类型与结构体类型类似但支持定义数据布局的多个_变体_variants。每个变体拥有自己独立的字段集合。枚举变体在表达式中被支持并带有测试、匹配与解构它们的工具。4.1 枚举类型声明一个枚举类型声明列出了不同的变体enum Shape { Circle{radius: u64}, Rectangle{width: u64, height: u64} }枚举变体可以有零个或多个字段。如果不给参数花括号也可以省略声明出简单的值enum Color { Red, Blue, Green }与结构体类型一样枚举类型可以拥有能力。例如Color枚举类型可以像原始数字类型一样声明为可复制、可丢弃、可存储enum Color has copy, drop, store, key { Red, Blue, Green }枚举类型还可以拥有key能力并作为全局存储中数据的根。枚举在此场景中的常见用途是数据版本化enum VersionedData has key { V1{name: String}, V2{name: String, age: u64}, }与结构体类似枚举类型可以是泛型的也可以接受位置参数。下面的类型表示一个泛型结果类型其变体构造器使用位置而非命名参数参见位置结构体enum ResultT has copy, drop, store { Err(u64), Ok(T) }4.2 构造枚举值枚举值的构造与结构体值类似let s: String; let data VersionedData::V1{name: s};如果枚举变体没有字段花括号可以省略let color Color::Blue;4.3 枚举变体的名称解析枚举的变体名称需要用枚举类型名限定如VersionedData::V1。注意目前use子句的别名尚不支持枚举变体但会在后续语言版本中加入。在某些情况下如下面的 match 表达式Move 编译器可以从上下文推断枚举类型此时类型名限定可以省略fun f(data: VersionedData) { match (data) { V1{..} .., ..} // 简单的变体名 OK }4.4 匹配枚举值match 表达式枚举值的值可以通过 match 表达式检查。例如fun area(self: Shape): u64 { match (self) { Circle{radius} mul_with_pi(*radius * *radius), Rectangle{width, height} *width * *height } }注意上面匹配的值是枚举值的不可变引用。match 表达式也可以消费一个值或对可变引用进行内部更新fun scale_radius(self: mut Shape, factor: u64) { match (self) { Circle{radius: r} *r *r * factor, _ {} // 如果不是 Circle 则什么都不做 } }match 表达式中提供的模式按文本出现顺序顺序求值直到找到匹配。如果所有已知模式没有被全部覆盖则属于编译期错误。模式可以嵌套并包含条件guardlet r : ResultResultu64 Ok(Err(42)); let v match (r) { Ok(Err(c)) if c 42 0, Ok(Err(c)) if c 42 1, Ok(_) 2, _ 3 }; assert!(v 1);注意在上面的例子中最后一个 match 子句_覆盖了Ok(Err(_))和Err(_)两种模式。虽然在运行时前面的子句对c的所有值都匹配Ok(Err(c))但由于条件的存在编译器无法确定所有情况都被覆盖match 表达式中的条件在追踪覆盖时不作考虑。因此上面 match 表达式中的前两个子句不足以满足匹配完整性需要额外的子句来避免编译错误。4.5 Match 表达式扩展自语言版本 2.4 起除了通用match扩展原始类型判别器、范围模式、通过引用匹配原始值见 Match 表达式Move 2.4 还增加了专门适用于结构体与枚举模式的进一步扩展嵌套在结构体和枚举变体模式中的字面量与范围模式通过或mut引用匹配结构体和枚举值包括嵌套字面量混合元组匹配某些位置是原始类型而其他位置不是的元组匹配。嵌套在结构体与枚举模式中的字面量与范围原始字面量和范围模式可以嵌套出现在结构体模式与枚举变体模式内部与变量绑定和_通配符自由混用。这对命名字段变体和位置变体都成立enum Inner has drop { A(u64), B } enum Outer has drop { W(Inner), X } fun deep(o: Outer): u64 { match (o) { Outer::W(Inner::A(1)) 100, Outer::W(Inner::A(_)) 200, Outer::W(Inner::B) 300, Outer::X 400, } } struct S has drop { x: u64, y: u64 } fun split(s: S): u64 { match (s) { S { x: 1, y } y 100, S { x: _, y: _ } 0, } } enum Pair has drop { P(u64, u64), Q } fun pair_match(p: Pair): u64 { match (p) { Pair::P(1, 2) 10, Pair::P(x, 2) x 100, Pair::P(_, _) 20, Pair::Q 30, } }范围模式在相同的位置上工作enum E has drop { V1(u64), V2 } fun bucket(e: E): u64 { match (e) { E::V1(0..100) 1, E::V1(100..999) 2, E::V1(_) 3, E::V2 4, } }通过引用匹配结构体与枚举match表达式可以以结构体或枚举值的不可变或可变引用作为判别器。变体或字段模式可以包含嵌套字面量与范围。此类模式中的变量绑定捕获的是内部字段的引用因此 arm 体中需要按通常方式解引用enum Pair has drop { P(u64, u64), Q } fun ref_match(p: Pair): u64 { match (p) { Pair::P(1, 2) 10, Pair::P(x, 2) *x 100, // x: u64 Pair::P(_, _) 20, Pair::Q 30, } }mut判别器以相同方式被支持。混合元组判别器Mixed-Tuple Discriminators判别器现在可以是某些位置为原始类型、某些位置不是的元组即_混合元组匹配_enum Data has drop { V1(u8), V2(u8) } fun make_pair(x: u8): (Data, u8) { (Data::V1(x), x) } fun classify(x: u8): u8 { match (make_pair(x)) { (Data::V1(a), 1) a 10, (Data::V2(a), 2) a 20, (Data::V1(a), y) if y 3 a y, _ 99, } }4.6 测试枚举变体is 运算符借助is运算符可以检查给定的枚举值是否属于某个变体let data: VersionedData; if (data is VersionedData::V1) { .. }该运算符允许指定由|分隔的变体列表。如果被测试表达式的类型已知变体可以不必用枚举名限定assert!(data is V1|V2);4.7 从枚举值中选取字段可以直接从枚举值中选取字段。回顾版本化数据的定义enum VersionedData has key { V1{name: String}, V2{name: String, age: u64}, }可以写出如下代码直接选取变体的字段let s: String; let data1 VersionedData::V1{name: s}; let data2 VersionedData::V2{name: s, age: 20}; assert!(data1.name data2.name); assert!(data2.age 20);注意如果枚举值没有带给定字段的变体字段选取会中止abort。例如data1.age就是这种情况。此中止使用的 abort code 是0xCA26CBD9BE0B0001。按照std::error约定该 code 属于std::error::INTERNAL类别reason 为1。字段选取仅当该字段在所有变体中具有唯一名称和唯一类型时才可能。因此下面的代码会产生编译期错误enum VersionedData has key { V1{name: String}, V2{name: u64}, } data.name // ^^^^^ 编译期错误name 字段选取有歧义4.8 在 let 中使用枚举模式枚举变体模式可以在let语句中使用let data: VersionData; let V1{name} data;解包枚举值时如果变体不是预期的那个会中止。为确保枚举的所有变体都被处理推荐使用match表达式而非let。match在编译期检查确保所有变体都被覆盖。在某些情况下像 Move Prover 这样的工具可以被用来验证let不会发生意外中止。4.9 通过模式匹配销毁枚举与结构体值类似枚举值可以通过显式解包来销毁。枚举可以通过以下方式解包match表达式中的模式、let绑定中的枚举模式或赋值中的枚举模式// 注意Shape 没有 drop 能力因此必须显式解包销毁。 enum Shape { Circle{radius: u64}, Rectangle{width: u64, height: u64} } fun destroy_empty(self: Shape) { match (self) { Shape::Circle{radius} assert!(radius 0), Shape::Rectangle{width, height: _} assert!(width 0), } } fun example_destroy_shapes() { let c Shape::Circle{radius: 0}; let r Shape::Rectangle{width: 0, height: 0}; c.destroy_empty(); r.destroy_empty(); }4.10 枚举的类型升级兼容性一个枚举类型可以被另一个枚举类型升级条件是新类型只在变体列表末尾新增变体。旧枚举类型中的所有变体必须出现在新类型中且保持相同顺序、从开头开始。考虑VersionedData类型它可能最初只有一个版本enum VersionedData has key { V1{name: String} }这个类型可以升级为我们目前为止使用的版本enum VersionedData has key { V1{name: String}, V2{name: String, age: u64}, }下面的升级不允许因为变体顺序必须保留enum VersionedData has key { V2{name: String, age: u64}, // 不是兼容的升级 V1{name: String}, }仓库印证枚举类型在 Aptos 框架中已被广泛使用。例如 market_types.move 中用enum OrderStatus has drop, copy, store表示订单状态、用enum OrderCancellationReason表示取消原因order_book.move 中甚至直接以enum OrderBookM: store copy drop has store作为全局存储根。这些真实代码印证了枚举在状态机建模、事件描述与数据版本化中的典型用途。五、结构体可见性Struct Visibility自语言版本 2.4 起默认情况下结构体的构造、销毁与字段访问都是模块私有的如特权结构体操作所述。Move 2.4 引入了显式可见性修饰符允许外部模块执行这些操作。5.1 语法将修饰符放在struct关键字之前module 0x42::shapes { // 任何模块都可访问 public struct Point { x: u64, y: u64, } // 同一包内的模块可访问 package struct Config { value: u64, } } module 0x42::lib { friend 0x42::consumer; // 仅声明的 friend 模块可访问 friend struct Token { amount: u64, } }5.2 跨模块访问具有足够可见性的外部模块可以构造和销毁结构体并读写其字段module 0x42::shapes { public struct Point { x: u64, y: u64, } } module 0x42::user { use 0x42::shapes::Point; fun mirror(p: Point): Point { let Point { x, y } p; // 解构 Point { x: y, y: x } // 构造 } fun shift_x(p: mut Point, delta: u64) { p.x p.x delta; // 读写字段 } }六、枚举可见性Enum Visibility自语言版本 2.4 起默认情况下枚举的构造、解构、匹配与字段选取都限制在定义模块内。Move 2.4 为枚举引入了显式可见性修饰符遵循与结构体与枚举可见性相同的模型。6.1 语法将修饰符放在enum关键字之前。对于带能力的枚举has子句照常跟在变体列表之后module 0x42::types { // 任何模块都可访问 public enum Color has copy, drop { Red, Green, Blue, } // 同一包内的模块可访问 package enum Status has drop { Active, Inactive, } } module 0x42::lib { friend 0x42::consumer; // 仅声明的 friend 模块可访问 friend enum Event has drop { Created, Updated, Deleted, } }6.2 跨模块访问外部模块具有足够可见性时可以构造和销毁枚举、测试变体、从变体中选取字段并修改它们module 0x42::types { public enum Shape has drop { Circle { radius: u64 }, Rectangle { width: u64, height: u64 }, } } module 0x42::user { use 0x42::types::Shape; fun area(s: Shape): u64 { match (s) { Shape::Circle { radius } radius * radius, Shape::Rectangle { width, height } width * height, } } fun is_circle(s: Shape): bool { s is Shape::Circle } }七、结构体与枚举的可见性Struct and Enum Visibility自语言版本 2.4 起默认情况下结构体和枚举的构造、销毁与字段访问都限制在定义模块内。Move 2.4 引入了允许外部模块执行这些操作的显式可见性修饰符。7.1 可见性级别有三个修饰符可用与函数可见性关键字对应修饰符可访问范围public任何模块package同一包内的所有模块friend在定义模块中被声明为friend的模块public(package)和public(friend)分别作为package和friend的别名被接受但不被鼓励未来将被弃用。请优先使用简写形式。7.2 性能考量跨模块类型操作目前被编译为函数调用而非直接字节码指令。因此它们还不是零成本抽象比在定义模块内执行的等价操作更昂贵。这一状况有望在未来通过 VM 改进而改变。在此之前只在确实需要跨模块访问时才使用可见性修饰符。7.3 限制Restrictions具有key能力的类型不能有可见性修饰符。作为全局存储根的类型必须保持模块私有以维护全局存储的访问控制// ERROR: 具有 key 能力的类型不能是 public、package 或 friend public struct Resource has key { value: u64 } public enum VersionedData has key { V1 { name: vectoru8 } }全局存储操作仍然仅限模块内。即使对public类型move_to、move_from、borrow_global和borrow_global_mut仍仅限于定义模块内使用。7.4 交易参数Transaction Arguments如果类型具有copy能力且没有key能力public结构体和枚举可以作为 entry 和 view 函数的参数传递。所有字段类型本身也必须是有效的参数类型递归地module 0x42::types { public struct Point has copy, drop { x: u64, y: u64, } public enum Direction has copy, drop { North, South, East, West, } // Point 和 Direction 都可以直接作为交易参数传递 entry fun move_to_point(s: signer, destination: Point) { .. } entry fun move_player(s: signer, dir: Direction) { .. } }7.5 可升级性Upgradability在包升级中结构体和枚举的可见性可以按以下方式变化。一般原则是不允许任何会破坏定义包外部代码的转换public永远不能被收窄但包内级别之间的转换是安全的因为它们的消费者位于同一包内并与升级一起原子地重新发布。私有类型可以升级为package、friend或publicfriend或package类型可以升级为publicfriend或package类型可以被收窄回私有。八、设计决策速查表综合全文在设计链上类型时可以参考以下决策路径业务需求推荐能力组合理由表示不可复制、不可丢失的资产如 Coinstore必要时加key防止复制与丢失允许存入全局存储普通数据容器行为接近其他语言的类copy, drop必要时加store可自由复制、丢弃作为全局存储根key内部字段需store可move_to/borrow_global多形态数据状态机、版本化数据使用enum 按需能力变体清晰表达状态match保证穷尽需要外部模块构造/解构/访问字段2.4public/package/friend修饰符提供跨模块能力的同时保留控制需要作为交易参数传入2.4publiccopy、无keyentry/view 函数参数的要求进一步阅读泛型与能力generics-and-abilities理解copy、drop、store、key四种能力的完整语义全局存储global-storagemove_to、move_from、borrow_global等操作条件与循环conditionals-and-loops通用 match 扩展与范围模式的完整语法实战参考coin.move、account.move、market_types.move 等框架源码中的类型设计范例。【免费下载链接】aptos-coreAptos is a layer 1 blockchain built to support the widespread use of blockchain through better technology and user experience.项目地址: https://gitcode.com/GitHub_Trending/ap/aptos-core创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表