Dynamic Field(动态字段)
- 将结构体和对象组合在一起的方式
- 可以将动态字段看作是在对象结构上没有明确定义的不可见字段
- 需要用到的模块以及函数:
-
use sui::dynamic_field;
-
例子: 在
Laptop
中添加动态字段Sticker
, key 为StickerName
rustpublic struct Laptop has key { id: UID, screen_size: u64, model: u64, } public struct StickerName has copy, drop, store { name: String, } public struct Sticker has key, store { id: UID, image_url: String, }
-
dynamic_field::add
(增)rustpublic fun add_sticker(laptop: &mut Laptop, name: String, image_url: String, ctx: &mut TxContext) { let sticker_name = StickerName {name}; let sticker = Sticker { id: object::new(ctx), image_url}; dynamic_field::add(&mut laptop.id,sticker_name, sticker); }
-
dynamic_field::remove
(删)rustpublic fun remove_sticker(laptop: &mut Laptop, name: String) { let sticker_name = StickerName { name }; dynamic_field::remove(&mut laptop.id, sticker_name); }
-
dynamic_field::borrow_mut
(改)rustpublic fun set_image_url(laptop: &mut Laptop, name: String, new_url: String) { let sticker_name = StickerName {name}; let sticker_mut_reference: &mut Sticker = dynamic_field::borrow_mut(&mut laptop.id, sticker_name); sticker_mut_reference.image_url = new_url; }
-
dynamic_field::borrow
(查)rustpublic fun read_image_url(laptop: &Laptop, name: String): String { let sticker_name = StickerName {name}; let sticker_reference: &Sticker = dynamic_field::borrow(&laptop.id, sticker_name); sticker_reference.image_url }
-
dynamic_field::exists_
: 根据 key 来检查 value 是否存在rustpublic fun extend_hat(sui_fren: &mut SuiFren, description: String, duration: u64) { if (dynamic_object_field::exists_(&sui_fren.id, string::utf8(HAT_KEY))) { let hat: &mut Hat = dynamic_field::borrow_mut(&mut sui_fren.id, string::utf8(HAT_KEY)); dynamic_field::add(&mut hat.id, EXTENSION_1, HatExtension1 { description, duration, }); }; }
-
Dynamic Field 使用场景 1: 管理合约状态
- 先来看一个例子:
-
为每个状态结构使用多个共享对象
ruststruct PriceConfigs has key { id: UID, price_range: vector<u64>, } struct StoreHours has key { id: UID, open_hours: vector<vector<u8>>, } struct SpecConfigs has key { id: UID, specs_range: vector<u64>, } fun init(ctx: &mut TxContext) { let price_configs = PriceConfigs { id: object::new(ctx), price_range: vector[1000, 5000], }; let store_hours = StoreHours { id: object::new(ctx), open_hours: vector[vector[9, 12], vector[1, 5]], }; let spec_configs = SpecConfigs { id: object::new(ctx), specs_range: vector[1000, 10000], }; transfer::share(price_configs); transfer::share(store_hours); transfer::share(spec_configs); }
-
一个购买 laptop 的函数(很长)
rustpublic fun purchase_laptop(price_configs: &PriceConfigs, store_hours: &SotreHours, &spec_configs: &SpecConfigs, laptop: String, price: u64, ctx: &mut TxContext) { }
-
使用动态对象字段优化
ruststruct StateConfigs has key { id: UID, } const PRICE_CONFIGS: vector<u8> = b"PRICE_CONFIGS"; struct PriceConfigs has store { price_range: vector<u64>, } const STORE_HOURS: vector<u8> = b"STORE_HOURS"; struct StoreHours has store { open_hours: vector<vector<u8>>, } const SPEC_CONFIGS: vector<u8> = b"SPEC_CONFIGS"; struct SpecConfigs has store { specs_range: vector<u64>, } fun init(ctx: &mut TxContext) { let state_configs = StateConfigs { id: object::new(ctx), }; dynamic_fields::add(&mut state_configs.id, PRICE_CONFIGS, PriceConfigs { id: object::new(ctx), price_range: vector[1000, 5000], }); dynamic_fields::add(&mut state_configs.id, STORE_HOURS, StoreHours { id: object::new(ctx), open_hours: vector[vector[9, 12], vector[1, 5]], }); dynamic_fields::add(&mut state_configs.id, SPEC_CONFIGS, SpecConfigs { id: object::new(ctx), specs_range: vector[1000, 10000], }); transfer::share(state_configs); }
-
优化后的购买 laptop 函数, 只需要传入 state_configs 即可, 对调用者友好
rustpublic fun purchase_laptop(state_configs: &StateConfigs, laptop: String, price: u64, ctx: &mut TxContext) { }
-
Dynamic Field 使用场景 2: 扩展/升级合约
-
首先需要了解 Sui官方文档中的兼容性政策和规则
-
即使在最宽松的政策下, 也不能改变任何现有的结构体
-
动态字段是唯一的方式,可以动态扩展现有的对象/结构,因此只需添加新函数或更新现有函数即可实现此功能
-
一般不建议添加超过10个动态字段,因为它们可能会散布在代码中,难以找到。有一种好方法可以解决这个问题,并且仍然能够轻松扩展现有的对象 → 将新添加的字段分组到一个单独的结构体中
rustuse sui::dynamic_field; struct Laptop has key { id: Id, } const EXTENSION_1: u64 = 1; struct PurchaseDetails has store { customer_name: String, street_address: String, price: u64, } public fun add_purchase_details(laptop: &mut Laptop, customer_name: String, street_address: String, price: u64) { dynamic_field::add(&mut laptop.id, EXTENSION_1, PurchaseDetails { customer_name, street_address, price, }); }
- 细节: 使用递增的键作为动态字段的 key
-
还可以使用相同的模式来增强已添加为另一个对象的动态对象字段的对象(Sticker)
rustuse sui::dynamic_object_field; use sui::dynamic_field; struct Laptop has key { id: Id, } struct Sticker has key, store { id: Id, } const EXTENSION_1: u64 = 1; struct StickerPurchaseDetails has store { customer_name: String, street_address: String, price: u64, } public fun add_sticker_purchase_details(laptop: &mut Laptop, sticker_name: String, customer_name: String, street_address: String, price: u64) { let sticker: &mut Sticker = dynamic_object_field::borrow_mut(laptop, sticker_name); dynamic_field::add(&mut sticker.id, EXTENSION_1, StickerPurchaseDetails { customer_name, street_address, price, }); }
Dynamic Object Field(动态对象字段)
Dynamic Object Field
与Dynamic Field
唯一的区别就是不会删除要添加的对象, Dynamic Field 会删除要添加的对象在区块链上的存储(链下会查不到 object id), 这与对象封装(object wrapping)时看到的副作用相同- 当一个 Object 作为
动态对象字段
添加到另一个 Object 后, 他的所有权归动态对象字段
本身所有 - 提供的函数与 Dynamic Field 一样, 在
dynamic_object_field
模块中
Dynamic Field vs Dynamic Object Field vs Object Wrapping
- Object Wrapping
- 将 对象 存放在另一个 对象 中(例如将 SuiFrens 放入 GiftBox 中)。这将从全局存储中移除被 wrap 的对象(SuiFrens), wrap 后离线网络用户界面无法查找它们。该 对象 将不再有任何所有者
- 你可以将这个想象成将一个 对象 转换为一个普通的 Non-Object
- Dynamic Field
- 也可以用来存储对象。这也会将对象从全局存储中移除。所有权也会被移除。与对象封装非常相似,只是字段是动态添加的,而不是在结构体中明确定义的
- Dynamic Object Field
- 不会从全局存储中移除对象。所有权转移到特殊的"动态字段对象",通常对于链下(Web用户界面)来说不容易查找。在大多数情况下,这与放弃对象所有权效果相同
- 开发者的角度
- 在结构体中是否应该明确定义该字段。这是使用动态字段的一个缺点,因为从对象结构定义中很难看出来。这意味着其他开发人员需要阅读整个模块的代码,以找到所有可能添加的动态字段。通常不建议添加超过10个独立的动态字段
- 是否应该从全局存储中移除该对象,从而在Web界面上不可见
Object owning Object
-
在复杂的应用设计中,有时需要让一个对象拥有另一个对象,以此建立起一种清晰的层次结构。比如,在一个包含多种对象类型的系统里,我们可能会遇到这样的情形:一个SuiFren拥有一个它正戴着的帽子
-
与其他三种对象组合的方式不同(对象封装、动态字段、动态对象字段), Object owning object 能够让对象拥有其他对象
-
缺点: 后期移除或修改比较麻烦
- 在关系很少或者从不改变的情况下, 适合使用 Object owning Object
-
例子
-
让 Laptop 对象拥有 Sticker 对象
ruststruct Laptop has key { id: UID, } struct Sticker has key, store { id: UID, image_url: String, } public fun add_sticker(laptop: &Laptop, sticker: Sticker) { transfer::public_transfer(sticker, object::uid_to_address(&laptop.id)); }
-
将 Laptop 对象的 Sticker 移除, 需要使用
Receiving<T>
与transfer::public_receive
rustpublic fun remove_sticker(laptop: &mut Laptop, sticker: Receiving<Sticker>) { let sticker = transfer::public_receive(&mut laptop.id, sticker); // Do something with the sticker }
-
在发送 Transaction 时,所有者可以指定贴纸
对象的地址
,Sui虚拟机将自动将其转换为Receiving<Sticker>
类型
-
-
回想一下上一章中的转让策略 , Obect owning Object 也受到该策略的限制, 只有具有 store 能力的对象, 才能够调用
transfer::public_receive
将其从定义其结构的 module 之外提取出来。 使用在定义其结构的 module 中, 使用transfer::receive
提取
Object 数据结构
Object_Bag
-
思考一个 GiftBox 中存放礼物的例子
-
只需要一个礼物某种类型的礼物
ruststruct GiftBox has key { id: UID, inner: SuiFren, } entry fun wrap_fren(fren: SuiFren, ctx: &mut TxContext) { let gift_box = GiftBox { id: object::new(ctx), inner: fren, }; transfer::transfer(gift_box, tx_context::sender(ctx)); }
-
我们想要礼物不仅仅为 1 个, 可以增加多个字段
ruststruct GiftBox has key { id: UID, inner_1: SuiFren, inner_2: SuiFren, inner_3: SuiFren, inner_4: SuiFren, inner_5: SuiFren, }
-
但是这样礼物数量始终是固定的, 这个时候可以使用
vector
ruststruct GiftBox has key { id: UID, frens: vector<SuiFren>, }
-
但是这样礼物类型始终是一样的, 这个时候可以使用
ObjectBag
rustuse sui::object_bag::{Self, ObjectBag}; struct MyBag has key { id: UID, object_bag: ObjectBag, } public fun create_bag(ctx: &mut TxContext) { transfer::transfer(MyBag { id: object::new(ctx), object_bag: object_bag::new(ctx), }, tx_context::sender(ctx)); } public fun add_to_bag<SomeObject>(my_bag: &mut MyBag, key: String, object: SomeObject) { object_bag::add(&mut my_bag.object_bag, key, object); }
-
-
ObjectBag
的语法和动态字段
很像
Object_Table
- 与
ObjectBag
不同,ObjectTable
只允许存储单一类型的对象 - 思考: 那为什么不直接用
vector
呢?- 虽然
ObjectTable
的功能有限,但当用户想要为表中的不同对象分配特定的键名时,它很有用
- 虽然
- 提供的函数: github.com/MystenLabs/...
rust
use sui::object_table::{Self, ObjectTable};
struct MyObject has key {
id: UID,
}
struct MyTable has key {
id: UID,
table: ObjectTable<String, MyObject>,
}
public fun create_table(ctx: &mut TxContext) {
transfer::transfer(MyTable {
id: object::new(ctx),
table: object_table::new(ctx),
}, tx_context::sender(ctx));
}
public fun add_to_table(my_bag: &mut MyBag, key: String, object: MyObject) {
object_table::add(&mut my_bag.object_bag, key, object);
}
Object id 与 address
-
从技术上来讲, Object id 和 address 是一样的, 都是 唯一标识符, 可以让我们识别并获得对象的数据(通过 id/address)
rustpublic fun get_object_id_from_address(object_addr: address): ID { object::id_from_address(object_addr) } public fun get_object_address(object: &MyObject): address { object::id_to_address(&object.id) }
-
object::new(ctx)
: 函数原型rustpublic fun new(ctx: &mut TxContext): UID { UID { id: ID { bytes: tx_context::fresh_object_address(ctx) }, } }
fresh_object_address
是Move中的一种特殊类型函数 - 原生函数- 原生函数是特殊的,因为它们的实现是作为Rust的一部分写入Move虚拟机中。这使得函数能够更快地运行,并且可以访问虚拟机的内部结构
fresh_object_address
可以看到用户的Transaction Payload
并使用它来为对象生成特殊地址。它还使用一个计数器来跟踪在同一交易中已经创建了多少个对象- 然后对交易有效载荷和计数器进行哈希运算,并确保结果在同一交易中创建的多个对象中是唯一的
-
利用生成的 object_id 的哈希值可以用作随机数函数的种子 (存在风险) (了解即可)
rustuse sui::bcs; public fun get_random_value(ctx: &mut TxContext): u64 { let object_id = object::new(ctx); let bytes = object::uid_to_bytes(&object_id); let random_number = bcs::peel_u64(&mut bcs::new(bytes)); object::delete(object_id); random_number }
- get_random_value可以被多次调用以生成伪随机数(看似随机)。这可以作为一般情况下获取随机值的源
- 用户可以更改 Transaction 中的到期时间戳和其他字段,以生成所需的对象 ID 和 随机数(例如赢得链上彩票)
- 再包含时间戳(来自系统对象Clock)可能更安全,但仍存在风险
- 用户仍然可以在一定程度上操纵随机值(尽管不像以前那样有对时钟对象的控制权)
- 验证器可以部分操纵随机值,同时也可以将时钟对象的时间戳设置为一个小范围内的特定值,误差较小
坑: 设计过多的对象
- 存在的问题
- 逻辑上需要合理
-
一个
Laptop电脑店
的例子ruststruct Laptop has key { id: UID, screen: Screen, keyboard: Keyboard, hard_drive: HardDrive, } struct Screen has key, store { id: UID, } struct Keyboard has key, store { id: UID, } struct HardDrive has key, store { id: UID, }
- 每个组件都是一个独立的对象。但是这不是必要的,因为屏幕、键盘和硬盘对象总是被封装在笔记本电脑对象中。它们是一个整体
- 只有当我们有意将键盘作为一个独立组件销售并集成到其他笔记本电脑中时,这种设计才有意义。如果我们经营的是一家硬件店,这可能是需要的,但我们只是一家笔记本电脑店
-
- 如果一个 Transaction 创建和改变了太多的对象,那么理解它就很困难。对于在浏览器界面上查看这些交易的用户来说,他们所看到的只是一个非常长的列表,列出了所有被更新或创建的对象,以及它们的所有相关字段/数据
- 如果用户需要与多个这些对象进行交互,那么构建 Transaction 时可能会变得复杂,因为界面需要找到并传递所有相关对象的地址
- 逻辑上需要合理
总结
动态字段和动态对象字段是在Sui Move合约开发中处理复杂状态和对象关系的两种强大工具。动态字段允许开发者在对象中动态添加、移除或修改没有在对象结构中明确定义的字段,而动态对象字段则用于在对象之间建立所有权和引用关系,但不会从全局存储中移除被引用的对象。
动态字段和动态对象字段主要用于以下场景:
- 管理合约状态:通过使用动态字段或对象字段,可以简化合约函数的参数,只需传入一个包含所有必要配置的对象,而不是多个分散的参数。
- 扩展/升级合约:即使在合约发布后,也可以通过添加新的动态字段来扩展合约的功能,而无需修改现有的结构体定义。这为合约提供了一定程度的灵活性和可扩展性。
- Object Owning Object:当需要明确地表示对象之间的所有权关系时,可以使用对象拥有对象的模式。这种模式下,一个对象可以直接拥有另一个对象,建立清晰的层次结构,但修改或移除关系可能较为复杂。
另外,Object Wrapping是另一种组合对象的方式,通过将一个对象存放在另一个对象中,可以在逻辑上建立它们之间的关系,但这会使被封装的对象从全局存储中移除,并且在Web界面上不可见。
最后,设计中要避免过度使用对象,尤其是在不需要独立存在的组件或当存在大量交互和依赖关系时。这不仅会增加理解和管理的复杂性,也可能影响用户交互和交易构建的简便性。
加入组织, 一起交流/学习!
- Sui 中文开发群(TG)
- 企鹅群: 79489587