儲存函式 (Storage Functions)
定義主要儲存操作的模組是 sui::transfer。它在所有依賴 Sui Framework 的套件中都會被隱式引入,因此,就像其他隱式引入的模組(例如 std::option 或 std::vector)一樣,不需要 use 陳述式。
快速參考請見 附錄 C:轉移函式 (Appendix C: Transfer Functions),其中列出了所有儲存函式與物件狀態。
總覽 (Overview)
transfer 模組為物件可以進入的每一種 所有權狀態 都提供了對應的函式:
- 轉移 (Transfer) - 將物件傳送給某個地址,使其進入 address owned(地址擁有)狀態;
- 凍結 (Freeze) - 將物件置於 immutable(不可變)狀態,使其成為永遠不會改變的 public constant(公開常數);
- 共享 (Share) - 將物件置於 shared(共享)狀態,讓所有人都能存取。
transfer 模組是大多數儲存操作的首選工具。有兩個特殊情況將另外說明:下一章的 動態欄位 (Dynamic Fields) — 將資料附加到物件上,以及本章末尾的 接收傳送給其他物件的物件。
擁有權與參考:快速回顧 (Ownership and References: a Quick Recap)
儲存函式直接建立在 擁有權與作用域 及 參考 章節的語意之上。所有這些函式都以傳值方式接收物件: 物件被移入函式中,呼叫者失去該物件的擁有權 — 而且,如我們即將看到的,該物件最終會以新的狀態進入儲存。這正是資源模型的運作方式:物件永遠不會被複製進儲存,而是被放置到儲存中,且先前的擁有者確實地放棄了它。另一方面,只需要讀取或更新物件的函式,則以參考的方式接收(&T 或 &mut T),並保持擁有權狀態不變。
Transfer 函式中的內部規則 (Internal Rule in Transfer Functions)
每個儲存操作都有兩種形式:internal(內部)和 public(公開)。內部函式 - transfer、share_object、freeze_object - 強制執行前一節的 internal constraint(內部約束):它們只能在定義該物件型別的模組中被呼叫。公開版本 - 以 public_ 為前綴 - 解除這個限制,但要求該型別除了 key 之外還要有 store:
/// 內部:只能在定義 `T` 的模組中呼叫。
public fun transfer<T: key>(obj: T, recipient: address);
/// 公開:可以從任何模組呼叫,但要求 `T` 具有 `store`。
public fun public_transfer<T: key + store>(obj: T, recipient: address);
這兩種形式共同實作了我們在 store ability 一節中預覽過的規則:只有 key 的物件的儲存,完全由其定義模組掌控,而 store 則讓該物件可以被任何模組執行儲存操作 - 以及由擁有者在交易中直接操作。
為了一次看清所有組合,假設模組 book::transfer_a 定義了兩個物件 - 具有 key 的 ObjectK 與具有 key + store 的 ObjectKS - 而模組 book::transfer_b 嘗試轉移 它們:
/// 從 `transfer_a` 匯入 `ObjectK` 與 `ObjectKS` 型別,並嘗試
/// 為它們實作不同的 `transfer` 函式。
module book::transfer_b;
// 這些型別對本模組來說並非內部!
use book::transfer_a::{ObjectK, ObjectKS};
// 失敗!`ObjectK` 不是本模組的內部型別。
public fun transfer_k(k: ObjectK, to: address) {
transfer::transfer(k, to);
}
// 失敗!`ObjectKS` 也不是本模組的內部型別 —
// `store` 不會影響內部函式。
public fun transfer_ks(ks: ObjectKS, to: address) {
transfer::transfer(ks, to);
}
// 失敗!`public_transfer` 要求 `store`,而 `ObjectK` 沒有它。
public fun public_transfer_k(k: ObjectK, to: address) {
transfer::public_transfer(k, to);
}
// 成功!`ObjectKS` 具有 `store`,而且這個函式是公開的。
public fun public_transfer_ks(ks: ObjectKS, to: address) {
transfer::public_transfer(ks, to);
}
同樣的矩陣也適用於 share_object/public_share_object 以及 freeze_object/public_freeze_object。理解這條規則,對於理解 Move 中的 應用程式設計至關重要:讓一個物件可公開轉移(key + store)與保持內部(僅 key)之間的抉擇,會大幅影響應用程式對其資產所能提供的保證。
Transfer 轉移 (Transfer)
transfer::transfer 函式會將物件送到某個地址,讓該地址成為其唯一擁有者:
module sui::transfer;
// 將 `obj` 轉移給 `recipient`。
public fun transfer<T: key>(obj: T, recipient: address);
// `transfer` 函式的公開版本。
public fun public_transfer<T: key + store>(obj: T, recipient: address);
在以下範例中,模組建立了一個代表應用程式管理員權限的物件,並將它送給模組的發布者:
/// A struct with `key` is an object. The first field is `id: UID`!
public struct AdminCap has key { id: UID }
/// `init` is a special function called once, when the module is
/// published. It is the best place to create singleton objects,
/// such as an admin capability.
fun init(ctx: &mut TxContext) {
// Create the `AdminCap` object in this scope.
let admin_cap = AdminCap { id: object::new(ctx) };
// Transfer the object to the transaction sender.
transfer::transfer(admin_cap, ctx.sender());
}
/// Transfers the `AdminCap` object to the `recipient`. Thus, the
/// recipient becomes the owner of the object, and only they can
/// access it.
public fun transfer_admin_cap(cap: AdminCap, recipient: address) {
transfer::transfer(cap, recipient);
}
當模組被發布時,init 函式會被呼叫,其中建立的 AdminCap 物件會被_轉移_給交易的發送者 —— ctx.sender() 會回傳目前交易的發送者地址。(init 函式在模組初始化器章節中有詳細說明。)
從此之後,假設發送者是 0xa11ce,該物件就處於_地址擁有_狀態:只有 0xa11ce 能在交易中使用它——無論是透過參考或值傳遞,包括用上方的 transfer_admin_cap 函式繼續轉移它。
地址擁有的物件受_真正擁有權_約束——只有擁有者地址能存取它們。這是 Sui 儲存模型中的基本概念,已在擁有權章節中介紹過。
公開轉移 (Public Transfer)
讓我們擴充範例,加入一個使用 AdminCap 來授權鑄造新物件並將其轉移給任意地址的函式:
/// Some `Gift` object that the admin can `mint_and_transfer`.
public struct Gift has key, store { id: UID }
/// Creates a new `Gift` object and transfers it to the `recipient`.
public fun mint_and_transfer(
_: &AdminCap,
recipient: address,
ctx: &mut TxContext,
) {
let gift = Gift { id: object::new(ctx) };
transfer::public_transfer(gift, recipient);
}
mint_and_transfer 函式「理論上」任何人都能呼叫——它是公開的——但它要求第一個參數必須是 AdminCap 參考,而 AdminCap 物件是由 0xa11ce 獨自擁有的。所以實際上只有 0xa11ce 能鑄造。這種簡單明確的方式來限制對函式的存取,就是_能力模式_,是 Sui 應用程式設計的基石之一。
注意這個範例中兩個物件的差異。AdminCap 只有 key:模組對它保有完全的控制權,如果模組沒有公開 transfer_admin_cap 函式,管理員權限就會是_靈魂綁定_的——無法轉讓出去。Gift 則具有 key + store:它是用 public_transfer 送出的,任何擁有 Gift 的人都能在自己的交易中自由地繼續轉移它,不需要這個模組的任何協助。
快速回顧 (Quick Recap)
- transfer 會將物件送到某個地址,使其成為_地址擁有_;
- 只有擁有者能使用地址擁有的物件——無論是透過參考或值傳遞;
- 要求一個只有 key 的物件作為參數,能將函式的存取權限制在物件擁有者身上——這就是_能力_模式;
- public_transfer 是公開版本:任何地方都能呼叫,但要求 key + store。
凍結 (Freeze)
transfer::freeze_object 函式會把物件轉為 不可變 狀態。物件一旦被 凍結,就永遠無法改變,任何人都可以透過不可變參考存取它:
module sui::transfer;
// 讓物件變成不可變,並允許任何人讀取它。
public fun freeze_object<T: key>(obj: T);
// `freeze_object` 函式的公開版本。
public fun public_freeze_object<T: key + store>(obj: T);
讓我們用一個由管理員建立並凍結的 Config 物件來延伸這個範例:
/// Some `Config` object that the admin can `create_and_freeze`.
public struct Config has key {
id: UID,
message: String,
}
/// Creates a new `Config` object and freezes it.
public fun create_and_freeze(
_: &AdminCap,
message: String,
ctx: &mut TxContext,
) {
let config = Config {
id: object::new(ctx),
message,
};
// Freeze the object so it becomes immutable.
transfer::freeze_object(config);
}
/// Returns the message from the `Config` object.
/// Can access the object by immutable reference!
public fun message(c: &Config): String { c.message }
一旦呼叫 create_and_freeze,Config 就會透過其 ID 公開可用,任何人都能呼叫 message 函式 —— 對於一個凍結的物件,不可變參考是人人都能自由取用的。
函式的定義與物件的狀態無關,因此定義一個以可變參考或以值取用凍結型別的函式, 在語法上完全合法 —— 只是這些函式無法用凍結物件來呼叫:
/// The function can be defined, but it won't be callable on a frozen
/// object - only immutable references to it are available.
public fun message_mut(c: &mut Config): &mut String { &mut c.message }
同樣的規則也適用於下方分享 (Share) 段落中定義的 delete_config:它以值取用 Config,而一個凍結的 Config 永遠無法傳入其中。凍結是_永久性_的: 一個凍結的物件無法被修改、轉移、刪除 —— 也無法解凍。
單一擁有者 → 凍結 (Owned → Frozen)
由於 freeze_object 的簽章接受任何以值傳入的物件,它既可以接收在同一作用域中 建立的物件,也可以接收發送者_擁有_的物件。從單一擁有者轉換為不可變狀態是可行的! 舉例來說,Gift 的擁有者可以決定將它永久保存:
/// Freezes the `Gift` object so it becomes immutable.
/// `Gift` has `key` + `store`, so `public_freeze_object` can be used!
public fun freeze_gift(gift: Gift) {
transfer::public_freeze_object(gift);
}
出於顯而易見的安全考量,反方向的情況也同樣值得留意:AdminCap 絕不能被凍結 —— 一旦被凍結的權限物件,就會變成任何人都可讀取,而每個以 &AdminCap 把關的 函式也會變成任何人都能呼叫。這再次凸顯了僅使用 key 這種模式的價值:AdminCap 沒有 store,外部程式碼無法將它凍結,而模組本身也根本不對外提供凍結函式。
快速回顧 (Quick Recap)
- freeze_object 會把物件轉為_不可變_狀態 —— 且是永久性的;
- 凍結的物件可供任何人透過不可變參考讀取,且永遠無法被修改、轉移或刪除;
- 已擁有的物件可以被凍結 —— 若物件具有 store,甚至可以由擁有者在交易中凍結;
- public_freeze_object 是公開版本:可在任何地方呼叫,但要求 key + store。
分享 (Share)
transfer::share_object 函式會將物件放入 共享 狀態,讓任何人都能透過可變參考(因此也包含不可變參考)存取它:
module sui::transfer;
/// 將物件放入共享狀態 — 讓所有人都能存取。
public fun share_object<T: key>(obj: T);
/// `share_object` 函式的公開版本。
public fun public_share_object<T: key + store>(obj: T);
/// Creates a new `Config` object and shares it.
public fun create_and_share(message: String, ctx: &mut TxContext) {
let config = Config {
id: object::new(ctx),
message,
};
// Share the object so it becomes shared.
transfer::share_object(config);
}
與同時接受新物件及已擁有物件的 freeze_object 不同,share_object 有一項執行期限制:只有在同一筆交易中建立的物件才能被共享。若嘗試共享一個已存在於擁有狀態的物件,交易會以 ESharedNonNewObject 中止。並不存在 Owned → Shared 的轉換:是否要讓物件成為共享狀態,必須在物件建立時就決定。而且和凍結一樣,共享是單向的——一旦共享,物件在其餘生都會維持共享狀態,唯一的例外我們接下來會看到。
特殊案例:共享物件的刪除 (Special Case: Shared Object Deletion)
雖然共享物件通常無法以值的方式取用,但有一種特殊情況例外——如果取用它的函式會將其刪除。這是 Sui 儲存模型中的一個特殊案例,用來允許清理共享狀態。讓我們新增一個刪除共享 Config 的函式:
/// Deletes the `Config` object, takes it by value.
/// Can be called on a shared object!
public fun delete_config(c: Config) {
let Config { id, message: _ } = c;
id.delete()
}
delete_config 函式以值的方式取用 Config,並將其完全銷毀——解構該結構並刪除 UID——Sui 驗證器允許這樣的呼叫。然而,若該函式回傳了 Config,或嘗試 transfer 或 freeze 它,交易就會被拒絕:
// 行不通!
public fun transfer_shared(c: Config, to: address) {
transfer::transfer(c, to);
}
規則:以值取用的共享物件,必須在同一筆交易中被刪除。
快速回顧 (Quick Recap)
- share_object 會將物件放入 共享 狀態,讓任何人都能透過可變參考存取;
- 只有在同一筆交易中建立的物件才能被共享——不存在 Owned → Shared 的轉換;
- 共享是永久性的,唯一的例外:共享物件可以被以值取用,以便進行 刪除;
- public_share_object 是公開形式:可在任何地方呼叫,需要 key + store。
Party Transfer 派對轉移 (Party Transfer)
transfer 模組還提供了 party_transfer 和 public_party_transfer,可將物件放入 party 狀態 —— 具備共識排序的單一擁有者存取模式。Party 物件是進階的較新功能,我們將其排除在本範例之外;函式簽名列於 附錄 C,詳細內容則涵蓋於 sui::party 模組文件中。
總結 (Summary)
| 函式 | 結果狀態 | 是否可逆 | 公開版本 |
|---|---|---|---|
| transfer | 地址擁有 | 是 - 可以再轉走 | public_transfer |
| freeze_object | 不可變 | 否 | public_freeze_object |
| share_object | 共享 | 只能透過刪除 | public_share_object |
| party_transfer | Party | 取決於權限 | public_party_transfer |
- 每個儲存函式都是以傳值方式接收物件——把物件放進儲存中會消耗它;
- 內部版本要求該型別必須定義在呼叫的模組中;public_* 版本則要求該型別具備 store 能力。
下一步 (Next Steps)
現在你已經了解 transfer 模組的主要功能,可以開始建構涉及儲存操作的應用程式了。在下一節中,我們會介紹 UID 與 ID 型別——每個物件的身分——之後則是 接收為物件 (Receiving as Object),也就是物件擁有其他物件背後的機制。