11.轻松入门Move: Bag和Table

上一章我们讲到使用动态字段可以给Person对象动态添加电子设备的例子,因为无法直接获取Person对象的动态字段个数,在删除Person对象之前,具体应该删除多少个动态字段也是不确定的,所以其实特别容易漏删,造成资源浪费。

Sui框架基于dynamic_field实现Bag和Table对象,解决了这个问题。Bag是一个异构的映射集合,也就是说值是键值对形式,而且每对键值对的类型可以不同。Table也是一个映射集合,但是所有键值对的类型必须一致。这一点从名字也有体现,包(bag)里可以塞任何东西,表格(Table)则只能按条目填写。

基于dynamic_object_field则实现了ObjectBag和ObjectTable。他们与Bag、Table的区别跟dynamic_field和dynamic_object_field的区别一样,这里不再赘述。本章我们以Bag和Table为例,讲解如何使用“升级版”动态字段。

我们先看Bag和Table的定义:

#![allow(unused)]
fn main() {
public struct Bag has key, store {
    /// the ID of this bag
    id: UID,
    /// the number of key-value pairs in the bag
    size: u64,
}
public struct Table<phantom K: copy + drop + store, phantom V: store> has key, store {
    /// the ID of this table
    id: UID,
    /// the number of key-value pairs in the table
    size: u64,
}
}

Bag和Table对象只有两个字段,其中size字段用于记录键值对的个数。

有的朋友可能会疑惑,这两个对象不都是映射集合嘛,怎么没有保存键值对的字段?别忘了我们讲到这俩对象是通过dynamic_field实现的。他们的add方法实现如下:

#![allow(unused)]
fn main() {
public fun add<K: copy + drop + store, V: store>(bag: &mut Bag, k: K, v: V) {
    field::add(&mut bag.id, k, v);
    bag.size = bag.size + 1;
}

public fun add<K: copy + drop + store, V: store>(table: &mut Table<K, V>, k: K, v: V) {
    field::add(&mut table.id, k, v);
    table.size = table.size + 1;
}
}

往Bag对象中添加键值对,本质就是往Bag对象添加动态字段,键作为动态字段的名称,值作为动态字段的值。我们上一节讲到每次调用dynamic_filed::add方法,都会创建一个Field对象,现在这个Field对象跟Bag对象关联,所以调用dynamic_filed::add的时候,传入的是bag的id,并且Bag对象对动态字段的数量进行了管理,每次新增+1,每次删除动态字段减一。Table对象也是如此,不再赘述。

所以官网说,Bag和Table不像传统的映射集合在其中保存键值对,它们的键值对作为对象保存在Sui的对象系统中,而Bag和Table只提供处理键值对的方法。那提供了哪些方法呢?

我们还是延用上一章的例子讲解Bag用法,人(Person)可以拥有多个不同种类的电子设备,比如笔记本、手机等。

#![allow(unused)]
fn main() {
//人对象定义
public struct Person has key {
    id: UID,
    name: String,
    electronic_devices: Bag,
}
//笔记本对象定义
public struct Notebook has key,store {
    id: UID,
    brand: String,
    model: String,
    weight: u64,
}
//手机对象定义
public struct MobilePhone has key,store {
    id: UID,
    brand: String,
    model: String,
    number: u64,
}
}

Person对象新增一个名为电子设备的字段,字段类型是Bag对象。我们来实例化一个Person对象:

#![allow(unused)]
fn main() {
public entry fun new(ctx: &mut TxContext) {
    transfer::transfer(
        Person{
            id: object::new(ctx),
            name: string::utf8(b"hanmeimei"),
            electronic_devices: bag::new(ctx),
        }, tx_context::sender(ctx)
    );
}
}

这里调用了bag::new方法,实例化了一个没有任何内容的电子设备。

添加键值对

假如现在购买了一个笔记本和一个手机,我们使用如下方法,给electronic_devices字段添加键值对:

#![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,
    };
    //键是vector<u8>类型,值是Notebook对象
    bag::add<vector<u8>, Notebook>(&mut person.electronic_devices, b"notebook_1", notebook);
}
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,
    };
    //u8类型,值是MobilePhone对象
    bag::add<u8, MobilePhone>(&mut person.electronic_devices, 1, mp);
}
}

两个方法都调用bag::add方法,往electronic_devices字段添加了不同类型的键值对。添加完两个电子设备,我们可以直接通查看Person对象,就可以获取这个对象拥有的电子设备数量:

访问键值对

跟其他类型一样,访问分为不可变访问和可变访问,使用哪个取决于是否需要改变键值对的值(注意这里只能改变值,不能改变键)。

#![allow(unused)]
fn main() {
public entry fun access_notebook(person: &Person, _: &mut TxContext) {
        assert!(bag::contains<vector<u8>>(&person.electronic_devices, b"notebook_1"), 1);
    	//不可变访问
        let notebook_ref = bag::borrow<vector<u8>, Notebook>(&person.electronic_devices, b"notebook_1");
        let _ = notebook_ref.brand;
    }
    public entry fun modify_notebook(person: &mut Person, _: &mut TxContext) {
        assert!(bag::contains<vector<u8>>(&person.electronic_devices, b"notebook_1"), 1);
        //可变访问
        let notebook_ref = bag::borrow_mut<vector<u8>, Notebook>(&mut person.electronic_devices, b"notebook_1");
        notebook_ref.brand = string::utf8(b"pingguo");
    }
}

在访问之前必须使用bag::contains判断是否存在该键值,否则会报错。使用bag::borrow方法会返回对Notebook对象的不可变引用,而bag::borrow_mut方法则是可变引用。

删除键值对
#![allow(unused)]
fn main() {
public entry fun remove_notebook(person: &mut Person, _: &mut TxContext) {
    assert!(bag::contains<vector<u8>>(&person.electronic_devices, b"notebook_1"), 1);
    let Notebook{id:id,brand:_,model:_,weight:_} = bag::remove<vector<u8>, Notebook>(&mut person.electronic_devices, b"notebook_1");
    id.delete();
}
}

为避免报错,删除之前也需要使用bag::contains确定是否包含该键。调用bag::remove方法会返回Notebook对象本身,我们可以选择删除这个对象或者转交。

注意,这里我使用的是id.delete()来删除对象这是Move 2024新增用法,是不是比原来的object::delete(id)顺眼?

删除Person对象

因为Bag是基于dynamic_field实现的,所以删除Person对象,也不会自动删除Bag内的键值对。所以删除Person对象之前也需要先删除键值对:

#![allow(unused)]
fn main() {
public entry fun delete_person(person: Person, _: &mut TxContext) {
    let Person{id:id, name: _, electronic_devices: mut electronic_devices} = person;

    assert!(bag::contains<vector<u8>>(&electronic_devices,  b"notebook_1"), 1);
    let Notebook{id:notebook_id,brand:_,model:_,weight:_} = bag::remove<vector<u8>, Notebook>(&mut electronic_devices, b"notebook_1");
    notebook_id.delete();

    assert!(bag::is_empty(&electronic_devices), 1);
    bag::destroy_empty(electronic_devices);
    id.delete();       
}
}

与上一章的删除Person对象不同,这里可以使用Bag提供的bag::is_empty方法判断是否已经删除所有的键值对,以此避免漏删。从这个角度来说我们应该尽量使用Bag而不是dynamic_field。

Table、ObjectTable、ObjectBag的用法都跟Bag一样,这里就不再赘述。总结来说Bag和Table其实只是一种在dynamic_field的基础上又封装了一层带有数量管理功能的对象。本文不仅仅介绍Bag和Table的用法,更是希望给读者展示Move如何通过装饰器模式扩展功能,希望读者能举一反三,开发出高效优美的Move代码。

了解更多Move内容:

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