在Move编程语言中,我们经常需要构建动态的数据结构来满足链上复杂业务需求,比如存储大量键值对或者处理异构数据。虽然动态字段(DynamicFields)提供了一些灵活性,但Move的Table和Bag是更进一步的解决方案,特别是在需要计数、保护对象免于意外删除时。这篇学习笔记将带
在 Move 编程语言中,我们经常需要构建动态的数据结构来满足链上复杂业务需求,比如存储大量键值对或者处理异构数据。虽然动态字段(Dynamic Fields)提供了一些灵活性,但 Move 的 Table 和 Bag 是更进一步的解决方案,特别是在需要计数、保护对象免于意外删除时。这篇学习笔记将带你全方位理解 Table 和 Bag 的核心机制,并通过丰富的代码实例来解析它们的用法。
在 Move 中,动态字段(Dynamic Fields)允许对象动态添加和移除字段。但存在以下问题:
因此,Table 和 Bag 应运而生:
module sui::table {
struct Table<K: copy + drop + store, V: store> has key, store { /* 内部实现 */ }
public fun new<K: copy + drop + store, V: store>(
ctx: &mut TxContext,
): Table<K, V>;
}
特点:
sui::table::new
函数并传入一个事务上下文 TxContext
,因为 Table 本质上是一个可以转移、共享和包裹的对象。module sui::bag {
struct Bag has key, store { /* 内部实现 */ }
public fun new(ctx: &mut TxContext): Bag;
}
特点:
注意:Bag 和 Table 的关系类似于动态字段中的 field 和 object field 的区别。Table 的 ObjectTable 版本允许值为对象类型,但这些对象在链上是可见的,而 Table 的普通版本对外隐藏了存储的值。
Table 和 Bag 提供了一组通用 API,涵盖了增、删、查、改操作。以下以 Table 为例进行讲解。
module sui::table {
public fun add<K: copy + drop + store, V: store>(
table: &mut Table<K, V>,
k: K,
v: V,
);
}
示例:
fun example_add(ctx: &mut TxContext) {
let mut table = sui::table::new<u64, u64>(ctx);
sui::table::add(&mut table, 1, 100); // 添加键 1,值 100
sui::table::add(&mut table, 2, 200); // 添加键 2,值 200
}
注意:如果尝试添加一个已存在的键,事务将失败。
module sui::table {
public fun borrow<K: copy + drop + store, V: store>(
table: &Table<K, V>,
k: K
): &V;
}
示例:
fun example_borrow(ctx: &mut TxContext) {
let mut table = sui::table::new<u64, u64>(ctx);
sui::table::add(&mut table, 1, 100);
let value = sui::table::borrow(&table, 1); // 借用键 1 的值
assert!(*value == 100, 0); // 验证值是否为 100
}
module sui::table {
public fun borrow_mut<K: copy + drop + store, V: store>(
table: &mut Table<K, V>,
k: K
): &mut V;
}
示例:
fun example_borrow_mut(ctx: &mut TxContext) {
let mut table = sui::table::new<u64, u64>(ctx);
sui::table::add(&mut table, 1, 100);
let value_mut = sui::table::borrow_mut(&mut table, 1);
*value_mut = 200; // 修改键 1 的值为 200
}
module sui::table {
public fun remove<K: copy + drop + store, V: store>(
table: &mut Table<K, V>,
k: K,
): V;
}
示例:
fun example_remove(ctx: &mut TxContext) {
let mut table = sui::table::new<u64, u64>(ctx);
sui::table::add(&mut table, 1, 100);
let value = sui::table::remove(&mut table, 1); // 删除键 1
assert!(value == 100, 0); // 验证删除的值是否为 100
}
module sui::table {
public fun length<K: copy + drop + store, V: store>(
table: &Table<K, V>,
): u64;
public fun is_empty<K: copy + drop + store, V: store>(
table: &Table<K, V>
): bool;
}
示例:
fun example_length(ctx: &mut TxContext) {
let mut table = sui::table::new<u64, u64>(ctx);
sui::table::add(&mut table, 1, 100);
let len = sui::table::length(&table); // 获取长度
assert!(len == 1, 0); // 验证长度是否为 1
}
Table 和 Bag 都无法直接通过作用域结束自动销毁,因为它们没有 drop
属性。需要显式调用以下函数销毁:
module sui::table {
public fun destroy_empty<K: copy + drop + store, V: store>(
table: Table<K, V>,
);
public fun drop<K: copy + drop + store, V: drop + store>(
table: Table<K, V>,
);
}
注意:destroy_empty
仅在集合为空时可用;而 drop
要求值类型 V
拥有 drop
能力。
特性 | Table | Bag |
---|---|---|
键值类型 | 同构 (K, V) | 异构 |
可见性 | 普通版本对外不可见;ObjectTable 可见 | 普通版本对外不可见;ObjectBag 可见 |
删除保护 | 支持 | 支持 |
使用场景 | 固定类型的键值对映射 | 动态类型的键值对集合 |
以下是一个综合示例,展示如何使用 Table 和 Bag 构建一个评分系统,每个用户可以对多个项目进行评分。
module example::rating_system {
use sui::table;
use sui::bag;
struct User has key, store {
id: UID,
ratings: Table<u64, u8>, // 每个用户存储项目评分
}
public fun add_user(ctx: &mut TxContext): User {
let user = User {
id: object::new_uid(ctx),
ratings: table::new(ctx),
};
user
}
public fun rate_project(user: &mut User, project_id: u64, rating: u8) {
assert!(rating <= 5, 0); // 评分范围:0-5
table::add(&mut user.ratings, project_id, rating);
}
public fun get_rating(user: &User, project_id: u64): u8 {
*table::borrow(&user.ratings, project_id)
}
public fun average_rating(user: &User): f64 {
let len = table::length(&user.ratings) as f64;
let mut total: u64 = 0;
for project_id in table::keys(&user
add_user
函数为每个用户创建一个 User
对象,并初始化一个 Table
来存储项目评分。rate_project
函数允许用户为指定的项目(以项目 ID 标识)提供评分(0-5)。get_rating
函数可以快速查询某个项目的评分。average_rating
函数遍历 Table
中的所有评分,并计算其平均值。在链上,资源的管理至关重要。Move 中,Table
和 Bag
的清理方式确保了链上数据的安全性。
如果集合为空,可以调用 destroy_empty
函数销毁它。
fun cleanup_empty_table(ctx: &mut TxContext) {
let mut table = sui::table::new<u64, u64>(ctx);
// 此时 Table 为空,可以销毁
sui::table::destroy_empty(table);
}
如果集合不为空,必须显式处理其值才能销毁。例如,drop
函数要求值类型支持 drop
。
fun cleanup_non_empty_table(ctx: &mut TxContext) {
let mut table = sui::table::new<u64, u64>(ctx);
sui::table::add(&mut table, 1, 100);
sui::table::add(&mut table, 2, 200);
// 显式处理每个值后,再销毁 Table
let value1 = sui::table::remove(&mut table, 1);
let value2 = sui::table::remove(&mut table, 2);
sui::table::drop(table); // 销毁
}
Bag
支持存储异构数据,并允许通过类型检查值的存在性。
module sui::bag {
public fun contains<K: copy + drop + store>(
bag: &Bag,
k: K
): bool;
public fun contains_with_type<K: copy + drop + store, V: store>(
bag: &Bag,
k: K
): bool;
}
示例:
fun example_bag_contains(ctx: &mut TxContext) {
let mut bag = sui::bag::new(ctx);
sui::bag::add(&mut bag, 1, 100u8);
sui::bag::add(&mut bag, "key", "value");
let contains_key = sui::bag::contains(&bag, "key");
let contains_key_with_type = sui::bag::contains_with_type<&str, &str>(&bag, "key");
assert!(contains_key, 0); // 验证键 "key" 是否存在
assert!(contains_key_with_type, 1); // 验证键 "key" 且值为字符串
}
表(Table)适合场景:
袋(Bag)适合场景:
功能对比 | Table | Bag |
---|---|---|
键值对类型 | 同构 | 异构 |
隐藏值 | 普通 Table 隐藏;ObjectTable 可见 | 普通 Bag 隐藏;ObjectBag 可见 |
类型检查支持 | 不支持 | 支持(通过 contains_with_type) |
以下示例展示如何使用 Bag
构建一个简单的 NFT 交易市场,其中每个卖家可以列出多个 NFT。
module marketplace::nft_market {
use sui::bag;
struct Seller has key, store {
id: UID,
nfts: Bag, // 使用 Bag 存储每个卖家的 NFT 列表
}
public fun new_seller(ctx: &mut TxContext): Seller {
let seller = Seller {
id: object::new_uid(ctx),
nfts: bag::new(ctx),
};
seller
}
public fun list_nft(seller: &mut Seller, nft_id: u64, price: u64) {
bag::add(&mut seller.nfts, nft_id, price); // 列出 NFT
}
public fun buy_nft(ctx: &mut TxContext, seller: &mut Seller, nft_id: u64) {
let price = bag::remove(&mut seller.nfts, nft_id); // 从卖家移除 NFT
transfer::transfer_tokens(ctx, price); // 模拟支付(具体实现略)
}
public fun get_listing(seller: &Seller, nft_id: u64): u64 {
*bag::borrow(&seller.nfts, nft_id) // 查询 NFT 的价格
}
}
通过 Move 的 Table
和 Bag
,我们可以高效地管理链上的动态集合。它们提供了比动态字段更强大的功能,尤其适合复杂数据结构的开发。在链上应用开发中,选择合适的数据结构不仅可以提升性能,还能降低出错的风险。
请用微信关注《HOH水分子》公众号,我们将持续分享和制作变成语言教程,让大家对编程产生化学反应。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!