Sui 数据类型讲解

这篇文章中,我们将介绍 Sui 中常见的数据结构,这些结构包含 Sui Move 和 Sui Framework 中提供的基础类型和数据结构,理解和熟悉这些数据结构对于 Sui Move 的理解和应用大有裨益。

首先,我们先快速复习一下 Sui Move 中使用到的基础类型。

无符号整型(Integer)

Move 包含六种无符号整型:u8u16 u32u64u128u256。值的范围从 0 到 与类型大小相关的最大值。

这些类型的字面值为数字序列(例如 112)或十六进制文字,例如 0xFF。 字面值的类型可以选择添加为后缀,例如 112u8。 如果未指定类型,编译器将尝试从使用文字的上下文中推断类型。 如果无法推断类型,则假定为 u64

对无符号整型支持的运算包括:

  • 算数运算: + - * % /

  • 位运算: & | ^ >> <<

  • 比较运算: > < >= <= == !=

  • 类型转换: as

    • 注意,类型转换不会截断,因此如果结果对于指定类型而言太大,转换将中止。

简单示例:

let a: u64 = 4;
let b = 2u64;
let hex_u64: u64 = 0xCAFE;

assert!(a+b==6, 0);
assert!(a-b==2, 0);
assert!(a*b==8, 0);
assert!(a/b==2, 0);

let complex_u8 = 1;
let _unused = 10 << complex_u8;

(b as u128)

布尔类型(Bool)

Move 布尔值包含两种,truefalse 。支持与 &&,或|| 和非 ! 运算。可以用于 Move 的控制流和 assert! 中。 assert! 是 Move 提供的用于断言,当判断的值是 false 时,程序会抛出错误并停止。

if (bool) { ... }
while (bool) { .. }
assert!(bool, u64)

地址(Address)

address 也是 Move 的原生类型,可以在地址下保存模块和资源。Sui 中地址的长度为 20 字节。

在表达式中,地址需要使用前缀 @ ,例如:

let a1: address = @0xDEADBEEF; 
let a2: address = @0x0000000000000000000000000000000000000002;

Tuples 和 Unit

Tuples 和 Unit () 在 Move 中主要用作函数返回值。只支持解构(destructuring)运算。

module ds::tuples {
    
    fun returns_unit() {}
    fun returns_2_values(): (bool, bool) { (true, false) }
    fun returns_4_values(x: &u64): (&u64, u8, u128, vector<u8>) { (x, 0, 1, b"foobar") }

    fun examples(cond: bool) {
        let () = ();
        let (x, y): (u8, u64) = (0, 1);
        let (a, b, c, d) = (@0x0, 0, false, b"");

        () = ();
        (x, y) = if (cond) (1, 2) else (3, 4);
        (a, b, c, d) = (@0x1, 1, true, b"1");
    }

    fun examples_with_function_calls() {
        let () = returns_unit();
        let (x, y): (bool, bool) = returns_2_values();
        let (a, b, c, d) = returns_4_values(&0);

        () = returns_unit();
        (x, y) = returns_2_values();
        (a, b, c, d) = returns_4_values(&1);
    }
}

接下来,我们从 Vector 开始,介绍 Sui 和 Sui Framework 中支持的集合类型。

数组(Vector)

vector<T> 是 Move 提供的唯一的原生集合类型。vector<T> 是由一组相同类型的值组成的数组,比如 vector<u64>vector<address> 等。

vector 支持的主要操作有:

  • 末尾添加元素:push_back
  • 末尾删除元素: pop_back
  • 读取或者修改: borrowborrow_mut
  • 判断是否包含: contains
  • 交换元素: swap
  • 读取元素索引: index_of
module ds::vectors {
    use std::vector;

    public entry fun example() {
        let v = vector::empty<u64>();
        vector::push_back(&mut v, 5);
        vector::push_back(&mut v, 6);

        assert!(vector::contains(&mut v, &5), 42);
        
        let (exists, index) = vector::index_of(&mut v, &5);
        assert!(exists, 42);
        assert!(index == 0, 42);

        assert!(*vector::borrow(&v, 0) == 5, 42);
        assert!(*vector::borrow(&v, 1) == 6, 42);

        vector::swap(&mut v, 0, 1);

        assert!(vector::pop_back(&mut v) == 5, 42);
        assert!(vector::pop_back(&mut v) == 6, 42);
    }
}

编译并运行示例:


sui client publish . --gas-budget 300000


export package_id=0xee2961ee26916285ebef57c68caaa5f67a3d8dbd

sui client call \
  --function example \
  --module vectors \
  --package ${package_id} \
  --gas-budget 30000

下面我们介绍几种基于 vector 的数据类型。

字符串(String)

Move 没有字符串的原生类型,但它使用 vector<u8> 表示字节数组。目前, vector<u8> 字面量有两种:字节字符串(byte strings)和十六进制字符串(hex strings)。

字节字符串是以 b 为前缀的字符串文字,例如 b"Hello!\n"

十六进制字符串是以 x 为前缀的字符串文字,例如 x"48656C6C6F210A" 。每一对字节的范围从 00FF,表示一个十六进制的 u8。因此我们可以知道: b"Hello" == x"48656C6C6F"

vector<u8> 的基础上,Move 提供了 string 包处理 UTF8 字符串的操作。

我们以创建 Name NFT 的为例:

module ds::strings {
    use sui::object::{Self, UID};
    use sui::tx_context::{sender, TxContext};
    use sui::transfer;

    
    use std::string::{Self, String};

    
    struct Name has key, store {
        id: UID,

        
        name: String
    }

    fun create_name(
        name_bytes: vector<u8>, ctx: &mut TxContext
    ): Name {
        Name {
            id: object::new(ctx),
            name: string::utf8(name_bytes)
        }
    }

    
    public entry fun issue_name_nft(
        name_bytes: vector<u8>, ctx: &mut TxContext
    ) {
        transfer::transfer(
            create_name(name_bytes, ctx),
            sender(ctx)
        );
    }
}

编译后命令行中调用:

$ sui client call \
  --function issue_name_nft \
  --module strings \
  --package ${package_id} \
  --args "my_nft" --gas-budget 30000



----- Transaction Effects ----
Status : Success
Created Objects:
  - ID: 0xf53891c8d200125bcfdba69557b158395bdf9390 , Owner: Account Address ( 0xf28e73e59f2305edf4df88756f78fa1f5d7e78b0 )
Mutated Objects:
  - ID: 0xd1de857a7a5452a73c9c176cd7c9db1b06671723 , Owner: Account Address ( 0xf28e73e59f2305edf4df88756f78fa1f5d7e78b0 )

可以在 Transaction Effects 中看到新创建的对象,ID 为 0xf53891c8d200125bcfdba69557b158395bdf9390,通过 Sui 提供的 RPC-API 接口 sui_getObject 可以看到其中保存的内容:

curl -H 'Content-Type: application/json' https://fullnode.devnet.sui.io:443 -d '{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "sui_getObject",
  "params":[
      "0xf53891c8d200125bcfdba69557b158395bdf9390"
  ]
}'

输出结果

{
    "jsonrpc": "2.0",
    "result": {
        "status": "Exists",
        "details": {
            "data": {
                "dataType": "moveObject",
                "type": "0xee2961ee26916285ebef57c68caaa5f67a3d8dbd::strings::Name",
                "has_public_transfer": true,
                "fields": {
                    "id": {
                        "id": "0xf53891c8d200125bcfdba69557b158395bdf9390"
                    },
                    "name": "my_nft"
                }
            },
            "owner": {
                "AddressOwner": "0xf28e73e59f2305edf4df88756f78fa1f5d7e78b0"
            },
            "previousTransaction": "7AfcBmJCioSbdZD6ZdYU2iUuGiSc62AuhZn7Yi3TfLDa",
            "storageRebate": 13,
            "reference": {
                "objectId": "0xf53891c8d200125bcfdba69557b158395bdf9390",
                "version": 1614,
                "digest": "/SEDlnh4xXq//ZGOCZVQM5QfyR2fPzJWaYWELhrSn2o="
            }
        }
    },
    "id": 1
}

VecMap 和 VecSet

Sui 在 vector 的基础上实现了两种数据结构,映射 vec_map 和集合 vec_set

vec_map 是一种映射结构,保证不包含重复的键,但是条目按照插入顺序排列,而不是按键的顺序。所有的操作时间复杂度为 0(N),N 为映射的大小。vec_map 只是为了提供方便的操作映射的接口,如果需要保存大型的映射,或者是需要按键的顺序排序的映射都需要另外处理。可以考虑使用之后介绍的 table 数据结构。

主要操作包括:

  • 创建空映射: empty
  • 插入键值对: insert
  • 获取键对应的值: getget_mut
  • 删除键: remove
  • 判断是否包含键: contains
  • 映射大小: size
  • 将映射转为键值对的数组: into_keys_values
  • 获取映射键的数组: keys
  • 删除空映射: destroy_empty
  • 通过插入的顺序索引键值对: get_entry_by_idxget_entry_by_idx_mut
module ds::v_map {
    use sui::vec_map;
    use std::vector;

    public entry fun example() {
        let m = vec_map::empty();
        let i = 0;
        while (i < 10) {
            let k = i + 2;
            let v = i + 5;
            vec_map::insert(&mut m, k, v);
            i = i + 1;
        };
        assert!(!vec_map::is_empty(&m), 0);
        assert!(vec_map::size(&m) == 10, 1);
        let i = 0;
        
        while (i < 10) {
            let k = i + 2;
            assert!(vec_map::contains(&m, &k), 2);
            let v = *vec_map::get(&m, &k);
            assert!(v == i + 5, 3);
            assert!(vec_map::get_idx(&m, &k) == i, 4);
            let (other_k, other_v) = vec_map::get_entry_by_idx(&m, i);
            assert!(*other_k == k, 5);
            assert!(*other_v == v, 6);
            i = i + 1;
        };
        
        let (keys, values) = vec_map::into_keys_values(copy m);
        let i = 0;
        while (i < 10) {
            let k = i + 2;
            let (other_k, v) = vec_map::remove(&mut m, &k);
            assert!(k == other_k, 7);
            assert!(v == i + 5, 8);
            assert!(*vector::borrow(&keys, i) == k, 9);
            assert!(*vector::borrow(&values, i) == v, 10);

            i = i + 1;
        }
    }
}

vec_set 结构保证其中不包含重复的键。所有的操作时间复杂度为 O(N),N 为映射的大小。同样, vec_set 提供了方便的集合操作接口,按插入顺序进行排序,如果需要使用按键进行排序的集合,也需要另外处理。

主要操作包括:

  • 创建空集合: empty
  • 插入元素: insert
  • 删除元素: remove
  • 判断是否包含元素: contains
  • 集合大小: size
  • 将集合转为元素的数组: into_keys
module ds::v_set {
    use sui::vec_set;
    use std::vector;

    public entry fun example() {
        let m = vec_set::empty();
        let i = 0;
        while (i < 10) {
            let k = i + 2;
            vec_set::insert(&mut m, k);
            i = i + 1;
        };
        assert!(!vec_set::is_empty(&m), 0);
        assert!(vec_set::size(&m) == 10, 1);
        let i = 0;
        
        while (i < 10) {
            let k = i + 2;
            assert!(vec_set::contains(&m, &k), 2);
            i = i + 1;
        };
        
        let keys = vec_set::into_keys(copy m);
        let i = 0;
        while (i < 10) {
            let k = i + 2;
            vec_set::remove(&mut m, &k);
            assert!(*vector::borrow(&keys, i) == k, 9);
            i = i + 1;
        }
    }
}

优先队列(PriorityQueue)

还有一种基于 vector 构建的数据结构:优先队列,他使用基于 vector 实现的大顶堆(max heap)来实现。

大顶堆是一种二叉树结构,每个节点的值都大于或等于其左右孩子节点的值,这样,这个二叉树的根节点始终都是所有节点中值最大的节点。

在优先队列中,我们为每一个节点赋予一个权重,我们基于权重构建一个大顶堆,从大顶堆顶部弹出根节点则为权重最大的节点。这样就形成过了一个按优先级弹出的队列。

优先队列主要包含的操作为:

  • 创建条目列表: create_entries ,结果作为 new 方法参数
  • 创建: new
  • 插入: insert
  • 弹出最大: pop_max

示例:

module ds::pq {
    use sui::priority_queue::{PriorityQueue, pop_max, create_entries, new, insert};

    
    fun check_pop_max(h: &mut PriorityQueue<u64>, expected_priority: u64, expected_value: u64) {
        let (priority, value) = pop_max(h);
        assert!(priority == expected_priority, 0);
        assert!(value == expected_value, 0);
    }

    public entry fun example() {
        let h = new(create_entries(vector[3, 1, 4, 2, 5, 2], vector[10, 20, 30, 40, 50, 60]));
        check_pop_max(&mut h, 5, 50);
        check_pop_max(&mut h, 4, 30);
        check_pop_max(&mut h, 3, 10);
        insert(&mut h, 7, 70);
        check_pop_max(&mut h, 7, 70);
        check_pop_max(&mut h, 2, 40);
        insert(&mut h, 0, 80);
        check_pop_max(&mut h, 2, 60);
        check_pop_max(&mut h, 1, 20);
        check_pop_max(&mut h, 0, 80);
    }
}

结构体(Struct)

Move语言中,结构体是包含类型化字段的用户定义数据结构。 结构可以存储任何非引用类型,包括其他结构。示例:

module ds::structs {
    
    struct Point has copy, drop, store {
        x: u64,
        y: u64,
    }
    
    struct Circle has copy, drop, store {
        center: Point,
        radius: u64,
    }
    
    public fun new_point(x: u64, y: u64): Point {
        Point {
            x, y
        }
    }
    
    public fun point_x(p: &Point): u64 {
        p.x
    }

    public fun point_y(p: &Point): u64 {
        p.y
    }

    fun abs_sub(a: u64, b: u64): u64 {
        if (a < b) {
            b - a
        }
        else {
            a - b
        }
    }
    
    public fun dist_squared(p1: &Point, p2: &Point): u64 {
        let dx = abs_sub(p1.x, p2.x);
        let dy = abs_sub(p1.y, p2.y);
        dx * dx + dy * dy
    }

    public fun new_circle(center: Point, radius: u64): Circle {
        Circle { center, radius }
    }
    
    public fun overlaps(c1: &Circle, c2: &Circle): bool {
        let d = dist_squared(&c1.center, &c2.center);
        let r1 = c1.radius;
        let r2 = c2.radius;
        d * d <= r1 * r1 + 2 * r1 * r2 + r2 * r2
    }
}

对象(Object)

对象是 Sui Move 中新引入的概念,也是 Sui 安全和高并发等众多特性的基础。定义一个对象,需要为结构体添加 key 能力,同时结构体的第一个字段必须是 UID 类型的 id。

对象结构中除了可以使用基础数据结构外,也可以包含另一个对象,即对象可以进行包装,在一个对象中使用另一个对象。

对象有不同的所有权形式,可以存放在一个地址下面,也可以设置成不可变对象或者全局对象。不可变对象永远不能被修改,转移或者删除,因此它不属于任何人,但也可以被任何人访问。比如合约包对象,Coin Metadata 对象。

我们可以通过 transfer 包中的方法对对象进行处理:

  • transfer:将对象放到某个地址下
  • freeze_object:创建不可变对象
  • share_object:创建共享对象
module ds::objects {
    use sui::object::{Self, UID};
    use sui::transfer;
    use sui::tx_context::{Self, TxContext};

    struct ColorObject has key {
        id: UID,
        red: u8,
        green: u8,
        blue: u8,
    }

    fun new(red: u8, green: u8, blue: u8, ctx: &mut TxContext): ColorObject {
        ColorObject {
            id: object::new(ctx),
            red,
            green,
            blue,
        }
    }

    public entry fun create(red: u8, green: u8, blue: u8, ctx: &mut TxContext) {
        let color_object = new(red, green, blue, ctx);
        transfer::transfer(color_object, tx_context::sender(ctx))
    }

    public fun get_color(self: &ColorObject): (u8, u8, u8) {
        (self.red, self.green, self.blue)
    }

    
    public entry fun copy_into(from_object: &ColorObject, into_object: &mut ColorObject) {
        into_object.red = from_object.red;
        into_object.green = from_object.green;
        into_object.blue = from_object.blue;
    }

    public entry fun delete(object: ColorObject) {
        let ColorObject { id, red: _, green: _, blue: _ } = object;
        object::delete(id);
    }

    public entry fun transfer(object: ColorObject, recipient: address) {
        transfer::transfer(object, recipient)
    }

    public entry fun freeze_object(object: ColorObject) {
        transfer::freeze_object(object)
    }

    public entry fun create_shareable(red: u8, green: u8, blue: u8, ctx: &mut TxContext) {
        let color_object = new(red, green, blue, ctx);
        transfer::share_object(color_object)
    }

    public entry fun create_immutable(red: u8, green: u8, blue: u8, ctx: &mut TxContext) {
        let color_object = new(red, green, blue, ctx);
        transfer::freeze_object(color_object)
    }

    public entry fun update(
        object: &mut ColorObject,
        red: u8, green: u8, blue: u8,
    ) {
        object.red = red;
        object.green = green;
        object.blue = blue;
    }
}

编译后调用:

  1. 创建共享对象
sui client call \
  --function create_shareable \
  --module objects \
  --package ${package_id} \
  --args 1 2 3 --gas-budget 30000


----- Transaction Effects ----
Status : Success
Created Objects:
  - ID: 0x3b25eba3bf836088b56bdfd36e39ec440db8bf59 , Owner: Shared
  1. 创建不可变对象
sui client call \
  --function create_immutable \
  --module objects \
  --package ${package_id} \
  --args 1 2 3 --gas-budget 30000


----- Transaction Effects ----
Status : Success
Created Objects:
  - ID: 0x88f8f210635af6503a8a07835ef12e147fa60aa3 , Owner: Immutable
  1. 将对象放入某个地址下
sui client call \
  --function create \
  --module objects \
  --package ${package_id} \
  --args 1 2 3 --gas-budget 30000


----- Transaction Effects ----
Status : Success
Created Objects:
  - ID: 0xf36144c71cde87c1e00f1bf00ee44653bc05228c , Owner: Account Address ( 0xf28e73e59f2305edf4df88756f78fa1f5d7e78b0 )

可以看到,不同所有权类型的对象会在创建时显示不同的类型结果。

  1. 修改共享对象或者是地址所拥有的对象:传入对象 ID 作为参数
sui client call \
  --function update \
  --module objects \
  --package ${package_id} \
  --args 0x3b25eba3bf836088b56bdfd36e39ec440db8bf59 4 5 6 --gas-budget 30000


----- Transaction Effects ----
Status : Success
Mutated Objects:
  - ID: 0x3b25eba3bf836088b56bdfd36e39ec440db8bf59 , Owner: Shared

可以在结果中看到 Mutated Objects 中对象已经发生了变化。

Dynamic field 和 Dynamic object field

对象虽然可以进行包装,但是也有一些局限,一是对象中的字段是有限的,在结构体定义是已经确定;二是包含其他对象的对象可能非常大,可能会导致交易 gas 很高,Sui 默认结构体大小限制为 2MB;再者,当遇到要储存不一样类型的对象集合时,问题就会比较棘手,Move 中的 vector 只能存储相同的类型的数据。

因此,Sui 提供了 dynamic field,可以使用任意名字做字段,也可以动态添加和删除。唯一影响的是 gas 的消耗。

dynamic field 包含两种类型,field 和 Object field,区别在于,field 可以存储任何有 store 能力的值,但是如果是对象的话,对象会被认为是被包装而不能通过 ID 被外部工具(浏览器,钱包等)访问;而 Object field 的值必须是对象(有 key 能力且第一个字段是 id: UID),对象仍然能从外部工具通过 ID 访问。

dynamic filed 的名称可以是任何拥有 copydropstore 能力的值,这些值包括 Move 中的基本类型(整数,布尔值,字节串),以及拥有 copydropstore 能力的结构体。

下面我们通过例子来看看具体的操作:

  • 添加字段: add
  • 访问和修改字段: borrowborow_mut
  • 删除字段
module ds::fields {
    use sui::object::{Self, UID};
    use sui::dynamic_object_field as dof;
    use sui::transfer;
    use sui::tx_context::{Self, TxContext};

    struct Parent has key {
        id: UID,
    }

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

    public entry fun initialize(ctx: &mut TxContext) {
        transfer::transfer(Parent { id: object::new(ctx) }, tx_context::sender(ctx));
        transfer::transfer(Child { id: object::new(ctx), count: 0 }, tx_context::sender(ctx));
    }

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

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

    public entry fun mutate_child_via_parent(parent: &mut Parent) {
        mutate_child(dof::borrow_mut<vector<u8>, Child>(
            &mut parent.id,
            b"child",
        ));
    }

    public entry fun delete_child(parent: &mut Parent) {
        let Child { id, count: _ } = dof::remove<vector<u8>, Child>(
            &mut parent.id,
            b"child",
        );
        object::delete(id);
    }

    public entry fun reclaim_child(parent: &mut Parent, ctx: &mut TxContext) {
        let child = dof::remove<vector<u8>, Child>(
            &mut parent.id,
            b"child",
        );
        transfer::transfer(child, tx_context::sender(ctx));
    }
}

编译并调用 initializeadd_child 方法:

sui client call \
  --function initialize \
  --module fields \
  --package ${package_id} \
  --gas-budget 30000


----- Transaction Effects ----
Status : Success
Created Objects:
  - ID: 0x55536ca8123ffb606398da9f7d2472888ca5bfd1 , Owner: Account Address ( 0xf28e73e59f2305edf4df88756f78fa1f5d7e78b0 )
  - ID: 0xf1206f0f7d97908aae907c23d69a4cd97120dc82 , Owner: Account Address ( 0xf28e73e59f2305edf4df88756f78fa1f5d7e78b0 )
sui client call \
  --function add_child \
  --module fields \
  --package ${package_id} \
  --args 0xf1206f0f7d97908aae907c23d69a4cd97120dc82 0x55536ca8123ffb606398da9f7d2472888ca5bfd1 --gas-budget 30000


----- Transaction Effects ----
Status : Success
Created Objects:
  - ID: 0xdf694f282f739f328325bc922b3083bd45f31cae , Owner: Object ID: ( 0xf1206f0f7d97908aae907c23d69a4cd97120dc82 )
Mutated Objects:
  - ID: 0x55536ca8123ffb606398da9f7d2472888ca5bfd1 , Owner: Object ID: ( 0xdf694f282f739f328325bc922b3083bd45f31cae )
  - ID: 0xf1206f0f7d97908aae907c23d69a4cd97120dc82 , Owner: Account Address ( 0xf28e73e59f2305edf4df88756f78fa1f5d7e78b0 )

可以通过 sui_getDynamicFields 方法查看添加的字段:

curl -H 'Content-Type: application/json' https://fullnode.devnet.sui.io:443 -d '{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "sui_getDynamicFields",
  "params":[
      "0xf1206f0f7d97908aae907c23d69a4cd97120dc82"
  ]
}'

结果:

{
    "jsonrpc": "2.0",
    "result": {
        "data": [
            {
                "name": "vector[99u8, 104u8, 105u8, 108u8, 100u8]",
                "type": "DynamicObject",
                "objectType": "0xee2961ee26916285ebef57c68caaa5f67a3d8dbd::fields::Child",
                "objectId": "0x55536ca8123ffb606398da9f7d2472888ca5bfd1",
                "version": 1621,
                "digest": "GNSaPghN+tRBkxKiVhQCn9jVBkjYV4RU4oF+c4CUGJM="
            }
        ],
        "nextCursor": null
    },
    "id": 1
}

其中 name“child” 。同时,对于对象 ID 0x55536ca8123ffb606398da9f7d2472888ca5bfd1,我们仍然能从链上追踪对应信息。

curl -H 'Content-Type: application/json' https://fullnode.devnet.sui.io:443 -d '{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "sui_getObject",
  "params":[
      "0x55536ca8123ffb606398da9f7d2472888ca5bfd1"
  ]
}'

集合数据类型

接下来,我们介绍几种基于 dynamic field 的集合数据类型。

前面介绍过,带有 dynamic field 的对象可以被删除,但是这对于链上集合类型来说这是不希望发生的,因为链上集合类型可能将无限多的键值对作为 dynamic field 保存。因此,在 Sui 提供了两种集合类型: TableBag,两者都基于 dynamic field 构建的映射类型的数据结构,但是额外支持计算它们包含的条目数,并防止在非空时意外删除。

TableBag 的区别在于,Table 是同质(*homogeneous)*映射,所以的键必须是同一个类型,所以的值也必须是同一个类型,而 Bag 是异质(heterogeneous)映射,可以存储任意类型的键值对。

同时,Sui 标准库中还包含对象版本的 TableBagObjectTableObjectBag,区别在于前者可以将任何 store 能力的值保存,但从外部存储查看时,作为值存储的对象将被隐藏,后者只能将对象作为值存储,但可以从外部存储中通过 ID 访问这些对象。

与之前介绍过的 vec_map 相比,table 更适合用来处理包含大量映射的情况。

Table

下面我们通过示例来展示对 table 的基本操作:

  • 添加元素: add
  • 读取和修改元素: borrowborrow_mut
  • 删除元素: delete
  • 元素长度: length
  • 判断存在性:contains

Object table 的操作与 table 类似。

module ds::tables {
    use sui::object::{Self, UID};
    use sui::transfer;
    use sui::tx_context::{Self, TxContext};
    use sui::table::{Self, Table};

    const EChildAlreadyExists: u64 = 0;
    const EChildNotExists: u64 = 1;

    struct Parent has key {
        id: UID,
        children: Table<u64, Child>,
    }

    struct Child has key, store {
        id: UID,
        age: u64
    }
    
    public entry fun initialize(ctx: &mut TxContext) {
        transfer::transfer(
            Parent { id: object::new(ctx), children: table::new(ctx) },
            tx_context::sender(ctx)
        );
    }

    public fun child_age(child: &Child): u64 {
        child.age
    }
    
    public fun child_age_via_parent(parent: &Parent, index: u64): u64 {
        assert!(!table::contains(&parent.children, index), EChildNotExists);
        table::borrow(&parent.children, index).age
    }
    
    public fun child_size_via_parent(parent: &Parent): u64 {
        table::length(&parent.children)
    }
    
    public entry fun add_child(parent: &mut Parent, index: u64, ctx: &mut TxContext) {
        assert!(table::contains(&parent.children, index), EChildAlreadyExists);
        table::add(&mut parent.children, index, Child { id: object::new(ctx), value: 0 });
    }
    
    public fun mutate_child(child: &mut Child) {
        child.age = child.age + 1;
    }

    public entry fun mutate_child_via_parent(parent: &mut Parent, index: u64) {
        mutate_child(table::borrow_mut(&mut parent.children, index));
    }
    
    public entry fun delete_child(parent: &mut Parent, index: u64) {
        assert!(!table::contains(&parent.children, index), EChildNotExists);
        let Child { id, age: _ } = table::remove(
            &mut parent.children,
            index
        );
        object::delete(id);
    }
}

Bag

Bag 的操作与 table 的操作接口类似:

  • 添加元素: add
  • 读取和修改元素: borrowborrow_mut
  • 删除元素: delete
  • 元素长度: length
  • 判断存在性:contains

这里我们仅展示添加不同类型的键值对。

Object_bag 的操作与 bag 类似。

module ds::bags {
    use sui::object::{Self, UID};
    use sui::transfer;
    use sui::tx_context::{Self, TxContext};
    use sui::bag::{Self, Bag};

    const EChildAlreadyExists: u64 = 0;
    const EChildNotExists: u64 = 1;

    struct Parent has key {
        id: UID,
        children: Bag,
    }

    struct Child1 has key, store {
        id: UID,
        value: u64
    }

    struct Child2 has key, store {
        id: UID,
        value: u64
    }

    public entry fun initialize(ctx: &mut TxContext) {
        transfer::transfer(
            Parent { id: object::new(ctx), children: bag::new(ctx) },
            tx_context::sender(ctx)
        );
    }
    
    public entry fun add_child1(parent: &mut Parent, index: u64, ctx: &mut TxContext) {
        assert!(bag::contains(&parent.children, index), EChildAlreadyExists);
        bag::add(&mut parent.children, index, Child1 { id: object::new(ctx), value: 0 });
    }
    
    public entry fun add_child2(parent: &mut Parent, index: u64, ctx: &mut TxContext) {
        assert!(bag::contains(&parent.children, index), EChildAlreadyExists);
        bag::add(&mut parent.children, index, Child2 { id: object::new(ctx), value: 0 });
    }
}

LinkedTable

linked_table 是另一种使用 dynamic field 实现的数据结构,它与 table 类似,除此之外,它还支持值的有序插入和删除。因此,除了 table 类似的基础操作方法,还包含 frontbackpush_frontpush_backpop_frontpop_back等操作,对于每一个键,也可以通过 prevnext 获取前一个和后一个插入的键。

module ds::linked_tables {
    use sui::linked_table::{
        Self,
        push_front,
        push_back,
        borrow,
        borrow_mut,
        remove,
        pop_front,
        pop_back,
        contains,
        is_empty,
        destroy_empty
    };
    use sui::tx_context::TxContext;

    public entry fun simple_all_functions(ctx: &mut TxContext) {
        let table = linked_table::new(ctx);
        
        push_back(&mut table, b"hello", 0);
        push_back(&mut table, b"goodbye", 1);
        
        
        assert!(contains(&table, b"hello"), 0);
        assert!(contains(&table, b"goodbye"), 0);
        assert!(!is_empty(&table), 0);
        
        *borrow_mut(&mut table, b"hello") = *borrow(&table, b"hello") * 2;
        *borrow_mut(&mut table, b"goodbye") = *borrow(&table, b"goodbye") * 2;
        
        assert!(*borrow(&table, b"hello") == 0, 0);
        assert!(*borrow(&table, b"goodbye") == 2, 0);
        
        push_front(&mut table, b"!!!", 2);
        
        
        push_back(&mut table, b"?", 3);
        
        
        let (front_k, front_v) = pop_front(&mut table);
        assert!(front_k == b"!!!", 0);
        assert!(front_v == 2, 0);
        
        assert!(remove(&mut table, b"goodbye") == 2, 0);
        
        
        let (back_k, back_v) = pop_back(&mut table);
        assert!(back_k == b"?", 0);
        assert!(back_v == 3, 0);
        
        assert!(remove(&mut table, b"hello") == 0, 0);
        
        assert!(is_empty(&table), 0);
        destroy_empty(table);
    }
}

TableVec

最后,我们介绍一种基于 table 的数据结构 table_vec。从名字就可以看出,table_vec 是使用 table 实现的可扩展 vector,它使用元素在 vector 的索引作为 table 中的键进行存储。table_vec 提供了与 vector 类似的操作方法。

module ds::table_vecs {
    use sui::table_vec;
    use sui::tx_context::TxContext;

    public entry fun example(ctx: &mut TxContext) {
        let vec = table_vec::singleton<u64>(1, ctx);

        table_vec::push_back(&mut vec, 2);
        assert!(table_vec::length(&vec) == 2, 0);

        let v = table_vec::borrow_mut(&mut vec, 1);
        *v = 3;

        assert!(table_vec::pop_back(&mut vec) == 3, 1);
        assert!(table_vec::pop_back(&mut vec) == 1, 1);

        assert!(table_vec::is_empty(&vec), 2);
        table_vec::destroy_empty(vec);
    }
}

编译并运行示例:

sui client call \
  --function example \
  --module table_vecs \
  --package ${package_id} \
  --gas-budget 30000

至此,我们介绍完了 Sui Move 中主要的数据类型及其使用方法,希望大家学习和理解 Sui Move 有一定的帮助。

Subscribe to qiwihui
Receive the latest updates directly to your inbox.
Mint this entry as an NFT to add it to your collection.
Verification
This entry has been permanently stored onchain and signed by its creator.