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);
}