动态(对象)字段
有多种方法可以使用对象字段存储原始数据和其他对象(封装),但是存在一些限制:
- 对象具有有限的由标识符键入的字段集,在发布其模块时是固定的(限于
struct
声明中的字段)。 - 如果一个对象包装了多个其他对象,它可能变得非常庞大。更大的对象可能导致交易中的 gas 费用更高。此外,对象大小存在一个上限。
- 存在需要存储异构类型对象集合的用例。由于 Move
vector
类型必须实例化为一个单一类型<T>
,因此不适用于这种情况。
幸运的是,Sui 提供了带有任意名称的动态字段(不仅仅是标识符),可以在运行时添加和删除(不在发布时固定),只在访问时影响 gas,并且可以存储异构值。使用本主题中的库与这种类型的字段进行交互。
字段与对象字段
有两种动态字段的类型 -- "字段" 和 "对象字段" -- 它们根据存储值的方式有所不同:
- 字段 可以存储任何具有
store
的值,但是存储在这种字段中的对象被视为已封装,并且无法通过其 ID 由外部工具(探测器、钱包等)访问存储。 - 对象字段 的值必须是对象(具有
key
能力,并且id: UID
是第一个字段),但仍然可以通过其 ID 由外部工具访问。
与这些字段进行交互的模块可以在 dynamic_field
和 dynamic_object_field
找到。
字段名称
与对象的常规字段不同,对象字段的名称必须是 Move 标识符,动态字段的名称可以是任何具有 copy
、drop
和 store
的值。这包括所有 Move 原语(整数、布尔值、字节字符串)和其内容具有 copy
、drop
和 store
的结构体。
添加动态字段
使用以下 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>
类型的字节字符串)的动态字段。此调用导致以下所有权关系:
- 发送方地址(仍然)拥有
Parent
对象。 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
是字段的名称。
sui::dynamic_object_field
具有等效的对象字段函数,但具有额外的约束 Value: key + store
。
要使用这些 API,可与之前定义的 Parent
和 Child
类型一起使用:
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
对象上调用。已添加到 Parent
的 Child
对象 必须 通过其动态字段访问,因此只能使用 mutate_child_via_parent
进行修改,而不能使用 mutate_child
,即使知道其 ID。
如果尝试借用不存在的字段,交易将失败。
传递给 borrow
和 borrow_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
的可变引用。如果在 object
的 name
处定义了一个具有 value: Value
的字段,则将其移除并返回 value
,否则将中止。将来尝试在 object
上访问此字段将失败。
sui::dynamic_object_field
具有等效的对象字段函数。
返回的值可以像任何其他值一样进行交互(因为它就是任何其他值)。例如,已删除的动态对象字段值可以通过 delete
或 transfer
转移到地址(返回给发送方):
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 提供了使用动态字段构建的 Table
和 Bag
集合,但还提供了额外的支持,以计算它们包含的条目数量,以防在非空时意外删除。要了解更多信息,请参阅 Tables and Bags。