Skip to content

对象模型

Sui 的核心创新:对象为中心的数据模型

本节重点

  1. Sui 对象的定义和特点是什么?
  2. 四种对象所有权模式有何区别?
  3. 如何创建、转移和删除对象?
  4. 共享对象和拥有对象的使用场景?
  5. 对象包装和动态字段如何工作?

什么是 Sui 对象?

Sui 对象是 Sui 区块链上的基本存储单元,每个对象都是独立的、可寻址的数据实体。

对象 vs 账户模型

优势:

  • 并行执行 - 不同对象的交易可并行处理
  • 简单性 - 每个对象独立,状态清晰
  • 安全性 - 明确的所有权语义
  • 可扩展 - 更好的水平扩展能力

对象定义

基本对象结构

move
module example::basic_object {
    use sui::object::{Self, UID};
    use sui::transfer;
    use sui::tx_context::{Self, TxContext};

    // Sui 对象必须包含:
    // 1. UID 字段(名为 id)
    // 2. key 能力
    struct MyObject has key {
        id: UID,        // 必需:对象唯一标识符
        value: u64,     // 自定义字段
        name: vector<u8>
    }

    // 创建对象
    public entry fun create(value: u64, name: vector<u8>, ctx: &mut TxContext) {
        let obj = MyObject {
            id: object::new(ctx),  // 生成新的 UID
            value,
            name
        };

        // 转移给调用者
        transfer::transfer(obj, tx_context::sender(ctx));
    }
}

对象的必要条件

必须满足:

  1. 第一个字段必须是 id: UID
  2. 必须有 key 能力
  3. 可以有其他能力(store

对象所有权

Sui 支持四种对象所有权模式:

拥有对象(Owned Object)

单一所有者拥有的对象

move
module example::owned {
    use sui::object::{Self, UID};
    use sui::transfer;
    use sui::tx_context::{Self, TxContext};

    struct OwnedObject has key {
        id: UID,
        value: u64
    }

    // 创建并转移给调用者
    public entry fun create(value: u64, ctx: &mut TxContext) {
        let obj = OwnedObject {
            id: object::new(ctx),
            value
        };

        transfer::transfer(obj, tx_context::sender(ctx));
    }

    // 转移给其他人
    public entry fun transfer_object(
        obj: OwnedObject,
        recipient: address
    ) {
        transfer::transfer(obj, recipient);
    }

    // 修改对象
    public entry fun update(obj: &mut OwnedObject, new_value: u64) {
        obj.value = new_value;
    }

    // 删除对象
    public entry fun delete(obj: OwnedObject) {
        let OwnedObject { id, value: _ } = obj;
        object::delete(id);
    }
}

特点:

  • ✅ 只有所有者能操作
  • ✅ 交易快速(无需共识)
  • ✅ 可并行处理
  • 🎯 用途:NFT、个人资产

共享对象(Shared Object)

多人可访问的对象

move
module example::shared {
    use sui::object::{Self, UID};
    use sui::transfer;
    use sui::tx_context::TxContext;

    struct Counter has key {
        id: UID,
        value: u64
    }

    // 创建共享对象
    public entry fun create(ctx: &mut TxContext) {
        let counter = Counter {
            id: object::new(ctx),
            value: 0
        };

        // 转为共享对象
        transfer::share_object(counter);
    }

    // 任何人都可以增加计数
    public entry fun increment(counter: &mut Counter) {
        counter.value = counter.value + 1;
    }

    // 读取计数
    public fun get_value(counter: &Counter): u64 {
        counter.value
    }
}

特点:

  • ✅ 多人可访问
  • ⚠️ 需要共识(略慢)
  • ⚠️ 不可删除(一旦共享,永久共享)
  • 🎯 用途:DeFi 池、市场、DAO

不可变对象(Immutable Object)

只读的对象

move
module example::immutable {
    use sui::object::{Self, UID};
    use sui::transfer;
    use sui::tx_context::TxContext;

    struct Config has key {
        id: UID,
        max_supply: u64,
        decimals: u8
    }

    // 创建不可变对象
    public entry fun create(
        max_supply: u64,
        decimals: u8,
        ctx: &mut TxContext
    ) {
        let config = Config {
            id: object::new(ctx),
            max_supply,
            decimals
        };

        // 冻结为不可变
        transfer::freeze_object(config);
    }

    // 只能读取
    public fun get_max_supply(config: &Config): u64 {
        config.max_supply
    }
}

特点:

  • ✅ 任何人可读
  • ❌ 无法修改
  • ❌ 无法删除
  • 🎯 用途:配置、元数据、常量

包装对象(Wrapped Object)

存储在其他对象内部的对象

move
module example::wrapped {
    use sui::object::{Self, UID};
    use sui::transfer;
    use sui::tx_context::{Self, TxContext};

    // 被包装的对象(需要 store 能力)
    struct InnerObject has key, store {
        id: UID,
        value: u64
    }

    // 包装其他对象的对象
    struct WrapperObject has key {
        id: UID,
        inner: InnerObject  // 包装的对象
    }

    // 创建并包装
    public entry fun create_wrapped(value: u64, ctx: &mut TxContext) {
        let inner = InnerObject {
            id: object::new(ctx),
            value
        };

        let wrapper = WrapperObject {
            id: object::new(ctx),
            inner
        };

        transfer::transfer(wrapper, tx_context::sender(ctx));
    }

    // 解包
    public entry fun unwrap(wrapper: WrapperObject, ctx: &mut TxContext) {
        let WrapperObject { id, inner } = wrapper;
        object::delete(id);

        // 转移内部对象
        transfer::transfer(inner, tx_context::sender(ctx));
    }
}

特点:

  • ✅ 对象组合
  • ✅ 内部对象不可直接访问
  • ✅ 可以解包
  • 🎯 用途:组合 NFT、质押凭证

对象操作

创建对象

move
module example::creation {
    use sui::object::{Self, UID};
    use sui::transfer;
    use sui::tx_context::{Self, TxContext};

    struct Item has key {
        id: UID,
        name: vector<u8>
    }

    // 方式 1:直接转移给调用者
    public entry fun create_and_transfer(name: vector<u8>, ctx: &mut TxContext) {
        let item = Item {
            id: object::new(ctx),
            name
        };
        transfer::transfer(item, tx_context::sender(ctx));
    }

    // 方式 2:转移给指定地址
    public entry fun create_for(
        name: vector<u8>,
        recipient: address,
        ctx: &mut TxContext
    ) {
        let item = Item {
            id: object::new(ctx),
            name
        };
        transfer::transfer(item, recipient);
    }

    // 方式 3:共享
    public entry fun create_shared(name: vector<u8>, ctx: &mut TxContext) {
        let item = Item {
            id: object::new(ctx),
            name
        };
        transfer::share_object(item);
    }

    // 方式 4:冻结
    public entry fun create_frozen(name: vector<u8>, ctx: &mut TxContext) {
        let item = Item {
            id: object::new(ctx),
            name
        };
        transfer::freeze_object(item);
    }
}

转移对象

move
module example::transfer_example {
    use sui::object::UID;
    use sui::transfer;

    struct Asset has key {
        id: UID,
        value: u64
    }

    // 转移拥有对象
    public entry fun transfer_owned(asset: Asset, recipient: address) {
        transfer::transfer(asset, recipient);
    }

    // 共享对象不能转移!
    // ❌ 这会编译错误
    // public entry fun transfer_shared(asset: Asset, recipient: address) {
    //     transfer::share_object(asset);  // 错误!
    // }

    // 使用公共转移
    public entry fun public_transfer(asset: Asset, recipient: address) {
        transfer::public_transfer(asset, recipient);
    }
}

删除对象

move
module example::deletion {
    use sui::object::{Self, UID};

    struct Temporary has key {
        id: UID,
        data: vector<u8>
    }

    // 删除对象
    public entry fun delete(obj: Temporary) {
        let Temporary { id, data: _ } = obj;
        object::delete(id);
    }

    // ❌ 不能删除共享对象
    // ❌ 不能删除不可变对象
    // ❌ 不能删除包装的对象(需先解包)
}

对象 ID

UID 和 ID

  • UID 是 唯一标识符,它是 Sui 中用于标识一个对象的关键数据类型,并且它是不可变的。
  • ID 是一个更通用的术语,它通常指代一种标识符,但不像 UID 那样用于唯一标识 Sui 中的对象。
move
module example::object_ids {
    use sui::object::{Self, UID, ID};
    use sui::transfer;
    use sui::tx_context::{Self, TxContext};

    struct MyObject has key {
        id: UID,
        value: u64
    }

    // UID -> ID 转换
    public fun get_id(obj: &MyObject): ID {
        object::uid_to_inner(&obj.id)
    }

    // 将 UID 转为地址
    public fun get_address(obj: &MyObject): address {
        object::uid_to_address(&obj.id)
    }

    // 从 ID 获取地址
    public fun id_to_address(id: ID): address {
        object::id_to_address(&id)
    }
}

这段代码主要的目的是进行不同标识符(UID、ID)和地址之间的转换。这种转换在 Sui 中非常有用,因为每个对象都有一个 UID,而 ID 和地址通常用于其他逻辑和操作(如账户操作或交易)。

对象查找

move
module example::lookup {
    use sui::object::{Self, UID, ID};
    use sui::dynamic_object_field as dof;

    struct Parent has key {
        id: UID
    }

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

    // 通过 ID 添加子对象
    public fun add_child(parent: &mut Parent, child: Child) {
        dof::add(&mut parent.id, object::id(&child), child);
    }

    // 通过 ID 查找子对象
    public fun get_child(parent: &Parent, child_id: ID): &Child {
        dof::borrow(&parent.id, child_id)
    }
}

实战示例

bash
# 创建新项目
sui move new example

# 进入项目目录
cd example

# 编写 toml 配置
[package]
name = "example"
version = "0.0.1"
edition = "2024.beta"

[dependencies]
Sui = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/sui-framework", rev = "mainnet" }

[addresses]
example = "0x0"

示例 1:简单 NFT

move
module example::simple_nft {
    use sui::object::{Self, UID};
    use sui::transfer;
    use sui::tx_context::{Self, TxContext};
    use std::string::{Self, String};

    // NFT 结构
    struct NFT has key, store {
        id: UID,
        name: String,
        description: String,
        image_url: String
    }

    // 铸造 NFT
    public entry fun mint(
        name: vector<u8>,
        description: vector<u8>,
        image_url: vector<u8>,
        ctx: &mut TxContext
    ) {
        let nft = NFT {
            id: object::new(ctx),
            name: string::utf8(name),
            description: string::utf8(description),
            image_url: string::utf8(image_url)
        };

        transfer::public_transfer(nft, tx_context::sender(ctx));
    }

    // 转移 NFT
    public entry fun transfer_nft(nft: NFT, recipient: address) {
        transfer::public_transfer(nft, recipient);
    }

    // 销毁 NFT
    public entry fun burn(nft: NFT) {
        let NFT { id, name: _, description: _, image_url: _ } = nft;
        object::delete(id);
    }

    // 查询函数
    public fun name(nft: &NFT): String {
        nft.name
    }

    public fun description(nft: &NFT): String {
        nft.description
    }

    public fun image_url(nft: &NFT): String {
        nft.image_url
    }
}
bash
# 编译 Move 合约
sui move build

# 部署合约
sui client publish --gas-budget 1000000000
bash
export PACKAGE_ID=???

# 铸造 NFT
sui client call --package $PACKAGE_ID \
--module simple_nft \
--function mint \
--args "First" "我在SUI链发行的第一张NFT" "ipfs://QmWWADHszhNSN7VfC4ENnN7ApSnCVU885d3o1BhwvryRcS" --gas-budget 100000000

# 转移 NFT
sui client call --package $PACKAGE_ID \
--module simple_nft \
--function transfer_nft \
--args "0x4e13" "0x3996" \
--gas-budget 100000000

# 链下读取NFT对象信息
$ sui client object 0x4e13285830dee23aa6d451f91af4a8d8928f2bfe525eafac9d25a63ad7bdaaec --json

示例 2:共享计数器

move
module example::shared_counter {
    use sui::object::{Self, UID};
    use sui::transfer;
    use sui::tx_context::TxContext;

    // 共享计数器
    struct Counter has key {
        id: UID,
        value: u64,
        owner: address  // 记录创建者
    }

    // 创建共享计数器
    public entry fun create(ctx: &mut TxContext) {
        let counter = Counter {
            id: object::new(ctx),
            value: 0,
            owner: tx_context::sender(ctx)
        };

        transfer::share_object(counter);
    }

    // 任何人都可以增加
    public entry fun increment(counter: &mut Counter) {
        counter.value = counter.value + 1;
    }

    // 只有所有者可以重置
    public entry fun reset(counter: &mut Counter, ctx: &mut TxContext) {
        assert!(counter.owner == tx_context::sender(ctx), 0);
        counter.value = 0;
    }

    // 查询
    public fun get_value(counter: &Counter): u64 {
        counter.value
    }
}
bash
# 编译 Move 合约
sui move build

# 部署合约
sui client publish --gas-budget 1000000000
bash
export PACKAGE_ID=???

# 任何人都可以创建计数器
sui client call --package $PACKAGE_ID \
--module shared_counter \
--function create \
--gas-budget 100000000

# 增加计数:任意钱包
sui client call --package $PACKAGE_ID \
--module shared_counter \
--function increment \
--args OBJECT_ID \
--gas-budget 10000000

# 链下读取NFT对象信息
$ sui client object OBJECT_ID --json

示例 3:对象包装(质押)

move
module example::staking {
    use sui::object::{Self, UID};
    use sui::transfer;
    use sui::tx_context::{Self, TxContext};
    use sui::coin::{Self, Coin};
    use sui::sui::SUI;

    // 质押凭证(包装了质押的币)
    struct StakeReceipt has key {
        id: UID,
        staked_amount: u64,
        staked_coin: Coin<SUI>,  // 包装的币
        stake_timestamp: u64
    }

    // 质押
    public entry fun stake(
        coin: Coin<SUI>,
        ctx: &mut TxContext
    ) {
        let amount = coin::value(&coin);

        let receipt = StakeReceipt {
            id: object::new(ctx),
            staked_amount: amount,
            staked_coin: coin,  // 包装币
            stake_timestamp: tx_context::epoch(ctx)
        };

        transfer::transfer(receipt, tx_context::sender(ctx));
    }

    // 解除质押
    public entry fun unstake(
        receipt: StakeReceipt,
        ctx: &mut TxContext
    ) {
        let StakeReceipt {
            id,
            staked_amount: _,
            staked_coin,
            stake_timestamp: _
        } = receipt;

        object::delete(id);

        // 返还币
        transfer::public_transfer(staked_coin, tx_context::sender(ctx));
    }

    // 查询质押信息
    public fun get_staked_amount(receipt: &StakeReceipt): u64 {
        receipt.staked_amount
    }
}

示例 4:市场(共享对象)

move
module example::marketplace {
    use sui::object::{Self, UID, ID};
    use sui::transfer;
    use sui::tx_context::{Self, TxContext};
    use sui::coin::{Self, Coin};
    use sui::sui::SUI;
    use sui::table::{Self, Table};

    // NFT 类型(示例)
    struct GameItem has key, store {
        id: UID,
        power: u64
    }

    // 挂单信息
    struct Listing has store {
        seller: address,
        price: u64,
        item_id: ID
    }

    // 市场(共享对象)
    struct Marketplace has key {
        id: UID,
        listings: Table<ID, Listing>  // item_id -> Listing
    }

    // 创建市场
    public entry fun create_marketplace(ctx: &mut TxContext) {
        let marketplace = Marketplace {
            id: object::new(ctx),
            listings: table::new(ctx)
        };

        transfer::share_object(marketplace);
    }

    // 挂单(将物品转移到市场)
    public entry fun list_item(
        marketplace: &mut Marketplace,
        item: GameItem,
        price: u64,
        ctx: &mut TxContext
    ) {
        let item_id = object::id(&item);

        let listing = Listing {
            seller: tx_context::sender(ctx),
            price,
            item_id
        };

        table::add(&mut marketplace.listings, item_id, listing);

        // 转移物品到市场对象
        transfer::public_transfer(item, object::uid_to_address(&marketplace.id));
    }

    // 购买(需要重新获取物品后实现)
    // 简化示例,实际实现需要动态对象字段
}

动态字段

Sui Move 中的「动态字段」(Dynamic Fields)是 Sui 最强大、最好用的特性之一,几乎所有中高级项目(NFT、游戏、DeFi、域名系统等)都离不开它。

动态字段到底是什么?

  • 它相当于一个「链上 Map / 字典」,Key → Value 可以动态增删。
  • Key 和 Value 都可以是任意类型(只要满足 storekey 约束)。
  • 它们不写死在对象结构体里,所以可以无限扩展,不会因为字段太多导致对象太大。
  • 动态字段本身也是对象!它们有自己的 object ID,独立存在。

动态字段的两种类型(官方分类)

类型用途Key 必须满足Value 必须满足典型场景
动态字段Value 有 store(可被别人拥有)key + storestoreNFT 属性、背包物品、用户数据
动态对象字段Value 没有 store(只能父对象拥有)key + storekey + store游戏中的怪物、房间、内部状态

核心 API:

move
module example {
    use sui::dynamic_field as df;
    /**
     * 伪代码演示
     */
     
    // 添加/覆盖
    df::add<KeyType, ValueType>(parent: &mut UID, key: KeyType, value: ValueType)

    // 借用(不可变)
    df::borrow<KeyType, ValueType>(parent: &UID, key: KeyType) -> &ValueType

    // 借用(可变)
    df::borrow_mut<KeyType, ValueType>(parent: &mut UID, key: KeyType) -> &mut ValueType

    // 删除并拿回 value(value 必须有 drop 或你手动处理)
    df::remove<KeyType, ValueType>(parent: &mut UID, key: KeyType) -> ValueType

    // 判断是否存在
    df::exists_<KeyType>(parent: &UID, key: KeyType) -> bool
}

动态字段 vs 动态对象字段

move
module example::dynamic_fields {
    use sui::object::{Self, UID};
    use sui::dynamic_field as df;
    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,
        value: u64
    }

    // 动态字段(值类型)
    public fun add_value_field(parent: &mut Parent) {
        df::add(&mut parent.id, b"key", 100u64);
    }

    public fun get_value_field(parent: &Parent): u64 {
        *df::borrow(&parent.id, b"key")
    }

    // 动态对象字段(对象类型)
    public fun add_object_field(parent: &mut Parent, child: Child) {
        let child_id = object::id(&child);
        dof::add(&mut parent.id, child_id, child);
    }

    public fun get_object_field(parent: &Parent, child_id: ID): &Child {
        dof::borrow(&parent.id, child_id)
    }

    // 删除字段
    public fun remove_field(parent: &mut Parent): u64 {
        df::remove(&mut parent.id, b"key")
    }
}

对象权限设计模式

在 Sui Move 中,权限设计一共有 7 大主流模式,从简单到高级,足以覆盖 99.9% 的真实项目需求。其中最常见、最核心的两种是:

  • 所有者检查(Owner Check)
  • 管理员凭证(AdminCap)

这两者加起来占了全网合约的 80% 以上。

所有者检查

在 Sui 生态里,如果你只懂一个权限模式,那就必须是「所有者检查(Owner Check)」。

它简单、直接、gas 最低、审计最友好,几乎所有个人资产、NFT、游戏角色、装备、背包…… 都是用它来保护的。

Sui 系统本身已经保证了 transfer::transfer(obj, recipient) 只能由 owner 调用。 但很多时候我们还希望提供自定义的 entry 函数(比如升级装备、改名、喂食宠物),这时就需要手动检查调用者是不是 owner。

move
module example::owner_check {
    use sui::object::UID;
    use sui::tx_context::{Self, TxContext};

    struct OwnedItem has key {
        id: UID,
        owner: address,
        value: u64
    }

    // 只有所有者可以修改
    public entry fun update(
        item: &mut OwnedItem,
        new_value: u64,
        ctx: &TxContext
    ) {
        assert!(item.owner == tx_context::sender(ctx), 0);
        item.value = new_value;
    }

    public entry fun transfer(item: OwnedItem, to: address) {
        transfer::transfer(item, to);
    }
}

管理员凭证

管理员凭证 AdminCap 是一个全局唯一、不可伪造、可以自由转移的对象,谁拥有它,谁就是管理员。

所有协议级操作(调费率、紧急暂停、提取资金、升级合约)都写成 _: &AdminCap 第一个参数,行业标准,一看就过审。

move
module example::admin {
    use sui::object::{Self, UID};
    use sui::transfer;
    use sui::tx_context::{Self, TxContext};

    // 管理员凭证 【推荐命名!!!】
    struct AdminCap has key {
        id: UID
    }

    // 受保护的资源
    struct ProtectedResource has key {
        id: UID,
        data: vector<u8>
    }

    // 初始化时创建管理员凭证
    fun init(ctx: &mut TxContext) {
        let admin_cap = AdminCap {
            id: object::new(ctx)
        };

        transfer::transfer(admin_cap, tx_context::sender(ctx));
    }

    // 只有持有 AdminCap 才能调用
    public entry fun admin_only(
        _admin_cap: &AdminCap,
        resource: &mut ProtectedResource,
        new_data: vector<u8>
    ) {
        resource.data = new_data;
    }
}

最佳实践

选择合适的所有权模式

move
// 1. 个人资产
public fun mint_nft(ctx: &mut TxContext) {
    let nft = NFT { id: object::new(ctx) };
    transfer::transfer(nft, tx_context::sender(ctx));   // 个人拥有
}

// 2. 共享对象
public fun create_pool(ctx: &mut TxContext) {
    let pool = DEXPool { id: object::new(ctx) };
    transfer::share_object(pool);                       // 所有人可用
}

// 3. 只读配置(部署时执行一次,永远不变)
public fun publish_game_config(ctx: &mut TxContext) {
    let config = GameConfig { id: object::new(ctx) };
    transfer::freeze_object(config);                    // 永久冻结
}

// 4. 组合资产(质押凭证)
public fun stake(coin: Coin<SUI>, ctx: &mut TxContext) {
    let receipt = StakeReceipt {
        id: object::new(ctx),
        staked: coin
    };
    transfer::transfer(receipt, tx_context::sender(ctx)); // 凭证归用户
}

对象能力设计

move
// ✅ 顶层对象:key
struct TopLevel has key { id: UID }

// ✅ 可嵌套对象:key + store
struct Nested has key, store { id: UID }

// ❌ 避免不必要的能力
// 资产类型不要加 copy 或 drop

删除对象

move
// ✅ 正确的删除方式
public entry fun delete(obj: MyObject) {
    let MyObject { id, data: _ } = obj;
    object::delete(id);
}

// ❌ 错误:忘记删除 UID
public entry fun wrong_delete(obj: MyObject) {
    let MyObject { id: _, data: _ } = obj;
    // UID 泄漏!
}

常见问题

Q1: 共享对象和拥有对象的性能差异?

A:

  • 拥有对象:无需共识,< 1秒确认
  • 共享对象:需要共识,1-2秒确认

使用场景:

  • 个人资产(NFT)→ 拥有对象
  • DeFi 池、市场 → 共享对象

Q2: 能否将共享对象转回拥有对象?

A: 不可以。一旦对象被共享,就永远是共享对象。

Q3: 包装对象有什么限制?

A: 限制条件

  • 被包装的对象必须有 store 能力
  • 包装后不可直接访问
  • 必须解包后才能转移或删除

Q4: 动态字段的 Gas 成本?

A: 动态字段的 Gas 成本高于静态字段。建议:

  • 已知的固定字段 → 使用静态字段
  • 不确定数量的数据 → 使用动态字段

Q5: 如何实现对象的"软删除"?

A: 使用标记字段:

move
struct Item has key {
    id: UID,
    deleted: bool  // 软删除标记
}

public entry fun soft_delete(item: &mut Item) {
    item.deleted = true;
}

基于 MIT 许可发布