Skip to main content

动态(对象)字段

有多种方法可以使用对象字段存储原始数据和其他对象(封装),但是存在一些限制:

  1. 对象具有有限的由标识符键入的字段集,在发布其模块时是固定的(限于 struct 声明中的字段)。
  2. 如果一个对象包装了多个其他对象,它可能变得非常庞大。更大的对象可能导致交易中的 gas 费用更高。此外,对象大小存在一个上限。
  3. 存在需要存储异构类型对象集合的用例。由于 Move vector 类型必须实例化为一个单一类型 <T>,因此不适用于这种情况。

幸运的是,Sui 提供了带有任意名称的动态字段(不仅仅是标识符),可以在运行时添加和删除(不在发布时固定),只在访问时影响 gas,并且可以存储异构值。使用本主题中的库与这种类型的字段进行交互。

字段与对象字段

有两种动态字段的类型 -- "字段" 和 "对象字段" -- 它们根据存储值的方式有所不同:

  • 字段 可以存储任何具有 store 的值,但是存储在这种字段中的对象被视为已封装,并且无法通过其 ID 由外部工具(探测器、钱包等)访问存储。
  • 对象字段 的值必须是对象(具有 key 能力,并且 id: UID 是第一个字段),但仍然可以通过其 ID 由外部工具访问。

与这些字段进行交互的模块可以在 dynamic_fielddynamic_object_field 找到。

字段名称

与对象的常规字段不同,对象字段的名称必须是 Move 标识符,动态字段的名称可以是任何具有 copydropstore 的值。这包括所有 Move 原语(整数、布尔值、字节字符串)和其内容具有 copydropstore 的结构体。

添加动态字段

使用以下 API 添加动态字段:

module sui::dynamic_field {

public fun add<Name: copy + drop + store, Value: store>(
object: &mut UID,
name: Name,
value: Value,
);

}
module sui::dynamic_object_field {

public fun add<Name: copy + drop + store, Value: key + store>(
object: &mut UID,
name: Name,
value: Value,
);

}

这些函数将一个具有名称 name 和值 value 的字段添加到 object。为了看到其运行情况,请考虑以下代码片段:

首先,为父对象和子对象定义两个对象类型:

struct Parent has key {
id: UID,
}

struct Child has key, store {
id: UID,
count: u64,
}

接下来,定义一个将 Child 对象作为 Parent 对象的动态字段添加的 API:

use sui::dynamic_object_field as ofield;

public fun add_child(parent: &mut Parent, child: Child) {
ofield::add(&mut parent.id, b"child", child);
}

此函数将 Child 对象按值获取,并将其作为 parent 的具有名称 b"child"(一种 vector<u8> 类型的字节字符串)的动态字段。此调用导致以下所有权关系:

  1. 发送方地址(仍然)拥有 Parent 对象。
  2. Parent 对象拥有 Child 对象,并且可以通过名称 b"child" 引用它。

覆盖字段是错误的(尝试添加与已定义的具有相同 <Name> 类型和值的字段),并且执行此操作的交易将失败。你可以通过可变引用对其进行原地修改,也可以通过首先删除旧值来安全地覆盖它们(例如更改其值类型)。

访问动态字段

你可以使用以下 API 引用动态字段:

module sui::dynamic_field {

public fun borrow<Name: copy + drop + store, Value: store>(
object: &UID,
name: Name,
): &Value;

public fun borrow_mut<Name: copy + drop + store, Value: store>(
object: &mut UID,
name: Name,
): &mut Value;

}

其中 object 是字段所定义的对象的 UID,name 是字段的名称。

info

sui::dynamic_object_field 具有等效的对象字段函数,但具有额外的约束 Value: key + store

要使用这些 API,可与之前定义的 ParentChild 类型一起使用:

use sui::dynamic_object_field as ofield;

public fun mutate_child(child: &mut Child) {
child.count = child.count + 1;
}

public fun mutate_child_via_parent(parent: &mut Parent) {
mutate_child(ofield::borrow_mut(
&mut parent.id,
b"child",
));
}

第一个函数接受对 Child 对象的可变引用,你可以使用尚未添加为 Parent 对象字段的 Child 对象调用它。

第二个函数接受对 Parent 对象的可变引用,并使用 borrow_mut 访问其动态字段,以传递给 mutate_child。这仅可在已定义 b"child" 字段的 Parent 对象上调用。已添加到 ParentChild 对象 必须 通过其动态字段访问,因此只能使用 mutate_child_via_parent 进行修改,而不能使用 mutate_child,即使知道其 ID。

tip

如果尝试借用不存在的字段,交易将失败。

传递给 borrowborrow_mut<Value> 类型必须与存储字段的类型匹配,否则交易将中止。

必须通过这些 API 访问动态对象字段的值。尝试将这些对象用作输入(按值或按引用)的交易将因为无效输入而被拒绝。

移除动态字段

类似于取消包装常规字段中的对象,你可以移除动态字段,公开其值:

module sui::dynamic_field {

public fun remove<Name: copy + drop + store, Value: store>(
object: &mut UID,
name: Name,
): Value;

}

此函数接受对 object 的 ID 和字段的 name 的可变引用。如果在 objectname 处定义了一个具有 value: Value 的字段,则将其移除并返回 value,否则将中止。将来尝试在 object 上访问此字段将失败。

tip

sui::dynamic_object_field 具有等效的对象字段函数。

返回的值可以像任何其他值一样进行交互(因为它就是任何其他值)。例如,已删除的动态对象字段值可以通过 deletetransfer 转移到地址(返回给发送方):

use sui::dynamic_object_field as ofield;
use sui::{object, transfer, tx_context};
use sui::tx_context::TxContext;

public fun delete_child(parent: &mut Parent) {
let Child { id, count: _ } = reclaim_child(parent);

object::delete(id);
}

public fun reclaim_child(parent: &mut Parent, ctx: &mut TxContext): Child {
ofield::remove(
&mut parent.id,
b"child",
);

}

类似于借用字段,尝试删除不存在的字段或具有不同 Value 类型的字段的交易将失败。

删除具有动态字段的对象

可以删除仍在其上定义(可能是非 drop)动态字段的对象。由于字段值只能通过动态字段的关联对象和字段名称访问,因此删除仍在其上定义动态字段的对象将使它们对未来交易全部不可访问。这一点与字段的值是否具有 drop 能力无关。当向对象添加少量静态已知的附加字段时,可能不会引起担忧,但对于可能包含无限多键值对作为动态字段的链上集合类型来说,这是特别不希望看到的情况。

Sui 提供了使用动态字段构建的 TableBag 集合,但还提供了额外的支持,以计算它们包含的条目数量,以防在非空时意外删除。要了解更多信息,请参阅 Tables and Bags