10.轻松入门Move: 动态字段

在第八章我们讲被嵌套的对象的时候,举了一个例子:人(Person对象)可能拥有0个或者多个笔记本电脑,但在实际生活中,我们不仅仅可以拥有笔记本电脑,我们还可以拥有平板电脑、手机、台式机、游戏机等电子设备。那Person对象的定义将会变成如下:

#![allow(unused)]
fn main() {
public struct Person has key {
    id: UID,
    name: String,
    //拥有的笔记本
    notebooks: vector<Notebook>,
    //拥有的手机
    mobile_phones: vector<MobilePhone>,
    //拥有的游戏机
    game_consoles: vector<GameConsole>,
    //拥有的平板电脑
    ipads: vector<Ipad>,
}
public struct Notebook has key,store {
    id: UID,
    brand: String,
    model: String,
}
public struct MobilePhone has key,store {
    id: UID,
    brand: String,
    model: String,
    number: u64,
}
public struct GameConsole has key,store {
    id: UID,
    brand: String,
    model: String,
    games: vector<u8>,
}
public struct Ipad has key,store {
    id: UID,
    brand: String,
    model: String,
    size: u64,
}
}

Person对象嵌套了一堆对象,但事实上有的老年人没有任何电子设备,那这个实例化这个老人的Person对象就得带着四个空的对象数组,不仅没有任何意义还会消耗GAS。再者如果每出现一个新型电子设备,就得给Person对象增加一个字段,不仅麻烦,后面Person对象的定义会变得又臭又长,难以维护。如果达到嵌套对象数量的上限,甚至会影响业务实现。

有没有一种方法,可以让对象只嵌套需要的对象,不限名称不限类型,还可以动态的嵌套,动态解除嵌套对象?

这时候就要用到动态字段了。

现在Person对象的类型定义就可以去掉所有电子设备相关的字段:

#![allow(unused)]
fn main() {
public struct Person has key {
    id: UID,
    name: String,
}
}

定义Notebook,MobilePhone等电子设备对象:

#![allow(unused)]
fn main() {
//注意:Notebook作为动态字段的值,必须具有store ability
public struct Notebook has key,store {
    id: UID,
    brand: String,
    model: String,
    weight: u64,
}
}

person对象购入一台笔记本电脑,只需调用add方法,给Person对象增加一个动态字段:

#![allow(unused)]
fn main() {
public entry fun add_notebook(person: &mut Person, ctx: &mut TxContext) {
    let notebook = Notebook{
        id: object::new(ctx),
        brand: string::utf8(b"brand"),
        model: string::utf8(b"model"),
        weight: 12,
    };
    //给Person对象增加一个动态字段
    ofield::add<String, Notebook>(&mut person.id, get_notebook_name(), notebook);
}
}

get_notebook_name()方法是用于获取String类型的动态字段名,而字段值是新实例化的Notebook对象。

add方法可以将不同类型的电子设备对象都加入到Person对象的动态字段中,而无需修改Person的类型定义。

如果要转卖笔记本电脑,就使用remove方法从Person对象中删除:

#![allow(unused)]
fn main() {
public fun remove_notebook(person: &mut Person, buyer: address, _:&mut TxContext) {
    //如果动态字段中不存在,就停止运行
    assert!(ofield::exists_<String>(&person.id, get_notebook_name()), ENotExsitsInOfiled);
	//remove方法从动态字段中删除
    let notebook:Notebook = ofield::remove<String, Notebook>(&mut person.id, get_notebook_name());
	//转交给买家
    transfer::public_transfer(notebook, buyer);
}
}

我们使用add和remove就可以灵活的给Person对象增加/删除字段,是不是特别简单。

但是,动态字段有两种,一种是dynamic_object_field,另一种则是dynamic_field。他们之间有什么区别呢,各自使用场景是什么?我们一起看看源码中add方法的定义:

#![allow(unused)]
fn main() {
//dynamic_object_field模块
public fun add<Name: copy + drop + store, Value: key + store>(
    object: &mut UID,
    name: Name,
    value: Value,
) 
}
#![allow(unused)]
fn main() {
//dynamic_field模块
public fun add<Name: copy + drop + store, Value: store>(
    object: &mut UID,
    name: Name,
    value: Value,
) 
}

两个模块对动态字段的字段名有相同的要求:必须是带有copy,drop,store 能力的数据类型。那就包括所有的基础类型和带有这三个能力的非对象结构体。在第9章中阐述了为什么对象不能有copy和drop能力,不懂的小伙伴可以翻阅一下这里不再赘述。

add函数的区别主要在于字段值。dynamic_object_field模块要求字段值必须是带有store能力的对象,而dynamic_field模块的字段值可以是带有store能力的任何数据类型。

所以如果字段值是非对象类型,就只能使用dynamic_field模块,但是如果值是对象,如何选择使用哪种动态字段呢?

假设现在Person对象有两个动态字段,一个值是笔记本另一个则是手机。分别使用两种动态字段添加到Person对象中。笔记本的代码已经在上面代码块add_notebook定义,就不再重复。

#![allow(unused)]
fn main() {
public struct MobilePhone has key,store {
    id: UID,
    brand: String,
    model: String,
    number: u64,
}
public entry fun add_mobilephone(person: &mut Person, ctx: &mut TxContext) {
    let mp = MobilePhone{
        id: object::new(ctx),
        brand: string::utf8(b"brand"),
        model: string::utf8(b"model"),
        number: 13512354569,
    };
    //使用dynamic_filed模块的add方法
    field::add<String, MobilePhone>(&mut person.id, get_mobilephone_name(), mp);
}
}

发布合约,再分别调用add_mobilephone方法和add_notebook方法,这两个方法都会创建一个归属于Person对象的Field对象,

这个Field对象是用于保存动态字段键值对的对象,它在源码中的定义如下:

#![allow(unused)]
fn main() {
public struct Field<Name: copy + drop + store, Value: store> has key {
    /// Determined by the hash of the object ID, the field name value and it's type,
    /// i.e. hash(parent.id || name || Name)
    id: UID,
    /// 键
    name: Name,
    /// 值
    value: Value,
}
}

调用add方法新创建的两个Filed对象类型分别是:Field<String, MobilePhone>和Filed<dynamic_object_field::Wrapper<0x1::string::String>, object::ID>。

这两个Filed对象的name字段类型不同,是为了让dynamic_object_field和dynamic_filed添加的动态字段的字段名区分开,避免产生键冲突。

使用dynamic_filed::add方法生成的Field对象,通过value字段直接嵌套了MobilePhone对象,那这个MobilePhone对象就只能通过Field对象进行访问,修改,删除和转移了。执行sui client object 命令也会报错不存在这个对象。

与此不同的是,dynamic_object_field:add对象生成的Field对象值是Notebook对象的ID,并没有嵌套Notebook对象,那就意味着外界依然可以访问Notebook对象。

所以对象选择哪种方式添加进动态字段,取决于被添加的对象是否需要被外界访问。

动态字段模块还为我们提供了borrow和borrow_mut方法来不可变引用和可变引用;exists_方法判断是否存在动态字段。

虽然是为Person对象添加的动态字段,但是删除Person对象并不会默认删除对象的动态字段!所以删除对象的方法里,应该先删除动态字段,再删除对象:

#![allow(unused)]
fn main() {
public entry fun delete_person(mut person: Person, _: &mut TxContext) {
    assert!(ofield::exists_<String>(&person.id, get_notebook_name()), ENotExsitsInOfiled);
    assert!(field::exists_<String>(&person.id, get_mobilephone_name()), ENotExsitsInOfiled);
	//删除notebook动态字段
    let Notebook{id: notebook_id, brand:_, model:_,weight:_} = ofield::remove<String, Notebook>(&mut person.id, get_notebook_name());
    object::delete(notebook_id);
	//删除mobilephone动态字段
    let MobilePhone{id: mobilephone_id, brand:_, model:_,number:_} = field::remove<String, MobilePhone>(&mut person.id, get_notebook_name());
    object::delete(mobilephone_id);
	//删除person对象
    let Person{id:id, name: _} = person;
    object::delete(id);
}
}

了解更多Move内容:

  • telegram: t.me/move_cn
  • QQ群: 79489587