Dynamic Fields, Tables & Collections

Struct fields are fixed at publish time, and an object has a maximum size.

30.1 Why dynamic fields? (Sui)

Struct fields are fixed at publish time, and an object has a maximum size. Dynamic fields attach arbitrary key-value data to an object's UID at runtime — heterogeneous, unbounded, and loaded only when accessed.

use sui::dynamic_field as df;
use sui::dynamic_object_field as dof;

public struct Character has key { id: UID }
public struct Hat has key, store { id: UID, color: u8 }
public struct StatKey has copy, drop, store { name: vector<u8> }

public fun add_stat(c: &mut Character, name: vector<u8>, value: u64) {
    df::add(&mut c.id, StatKey { name }, value);
}

public fun stat(c: &Character, name: vector<u8>): u64 {
    *df::borrow(&c.id, StatKey { name })
}

public fun bump_stat(c: &mut Character, name: vector<u8>) {
    let v: &mut u64 = df::borrow_mut(&mut c.id, StatKey { name });
    *v = *v + 1;
}

public fun wear_hat(c: &mut Character, hat: Hat) {
    dof::add(&mut c.id, b"hat", hat);           // child object stays addressable by its ID
}

public fun remove_hat(c: &mut Character): Hat {
    dof::remove(&mut c.id, b"hat")
}

30.2 Dynamic field vs dynamic object field

| | dynamic_field | dynamic_object_field | |-|-----------------|------------------------| | Value type | Any store type | Must be key + store (an object) | | Child visible by ID in explorers/RPC | No (value is wrapped) | Yes | | Typical use | Counters, metadata, balances | Attaching objects (items, NFTs) | | API | add, borrow, borrow_mut, remove, exists_, exists_with_type, remove_if_exists | add, borrow, borrow_mut, remove, exists_, id |

Names can be any copy + drop + store value. Define a dedicated key struct per field kind to avoid collisions. When deleting a parent, remove dynamic fields first; otherwise they become inaccessible (and their storage is not rebated).

30.3 Sui collection types

Small, bounded collections sui::vec_map::VecMap<K, V> Small maps; O(n) lookup; copy/drop if K, V allow sui::table::Table<K, V>

Large homogeneous maps; must be empty to destroy sui::object_table::ObjectTable<K, V> Dynamic object fields Values stay addressable objects

sui::bag::Bag / object_bag::ObjectBag sui::linked_table::LinkedTable<K, V> Iterable, ordered

use sui::table::{Self, Table};

public struct Registry has key { id: UID, names: Table<address, vector<u8>> }

public fun register(r: &mut Registry, name: vector<u8>, ctx: &TxContext) {
    let who = ctx.sender();
    if (r.names.contains(who)) {
        *r.names.borrow_mut(who) = name;
    } else {
        r.names.add(who, name);
    }
}

30.4 Aptos collections

Each entry a separate storage slot; not iterable SmartTable<K, V>

Bucketed hash table; iterable; fewer slots SimpleMap<K, V>

std::simple_map / aptos_std::simple_map Vector-based, small maps OrderedMap / BigOrderedMap aptos_std (newer)

Sorted maps; big variant is storage-scalable B+ tree

use aptos_std::table::{Self, Table};

struct Registry has key { names: Table<address, String> }

public entry fun register(user: &signer, name: String) acquires Registry {
    let reg = borrow_global_mut<Registry>(@my_addr);
    table::upsert(&mut reg.names, std::signer::address_of(user), name);
}