模拟引用
在 Sui 区块链上,一切都是对象。当你为 Sui 网络开发 Move 包时,通常通过 Sui API 中可用的功能以某种方式操纵或使用链上对象。对于大多数 API 函数,你提供一个对象的引用。
引用是在 Move 和 Sui 中编程时的关键构造。Sui API 中可用的大多数功能都使用对象的引用。
有两种使用对象的方式:
- 按值使用: 当你按值使用对象时,你对该对象拥有完全控制权。你可以销毁它(如果功能可用),包装它(如果它具有
store
能力)或将其传输到地址。 - 按引用使用: 当你按引用使用对象时,对该对象的操作由定义对象的模块提供的逻辑确定,因为你是使用对其数据的引用,而不是拥有对象本身。引用的限制使你能够在资产周围开发具有高安全性和安全性水平的智能合约。有两种引用类型:
- 可变引用(
&mut
):你可以更改对象(根据 API),但不能销毁或转移它。 - 不可变引用(
&
):进一步限制了对引用对象的操作和对引用对象的数据的保证/不变性。你只能对对象的数据进行只读访问。
- 可变引用(
可编程事务块(PTB)目前不允许使用从其事务命令之一返回的对象引用。你可以将输入对象用于 PTB、由 PTB 创建的对象(例如 MakeMoveVec
)或者从事务命令中按值返回的对象作为后续事务命令中的引用。然而,如果事务命令返回引用,则无法在任何调用中使用该引用,从而显著限制了 Move 中某些常见模式的使用。
borrow 模块
Sui 框架包含一个 borrow 模块,提供了对引用问题的解决方案。该模块通过值提供对对象的访问,但构建了一个模型,使其不可能销毁、转移或包装检索到的对象。borrow 模块公开了一个 Referent
对象,该对象包装另一个对象(你想要引用的对象)。该模块使用 热土豆模式(通过 Borrow
实例)允许以值的形式检索包装的对象。在同一 PTB 中,该模块然后强制对象被返回给 Referent
。Borrow
实例保证返回的对象与检索到的对象相同。
例如,请考虑以下模块存根,该模块公开一个对象(Asset
)和一个函数(use_asset
)来使用该对象。
module a_module {
struct Asset has key, store {
… // some data
}
public fun use_asset(asset: &Asset) {
…. // some code
}
}
函数 use_asset
接受对资产的不可变引用 (&Asset
),这是 API 定义中的一种常见模式。
现在考虑另一个使用该资产的模块。
module another_module {
struct AssetManager has key {
asset: Asset,
}
public fun get_asset(manager: &AssetManager): &Asset {
&manager.asset
}
}
该模块创建了一个对象 (AssetManager
),它引用了前一个模块 (a_module
) 中创建的对象 (Asset
)。
然后,你可以编写一个 Move 函数,通过引用检索对象并将其传递给 use_asset
函数。
fun do_something(manager: &AssetManager) {
let asset = another_module::get_asset(manager);
a_module::use_asset(asset);
}
do_something
中的两个函数在 PTB 中是无效的,因为 PTB 不支持从函数返回的引用并传递给另一个函数。
为了使这个操作在 PTB 中有效,你需要包含来自 borrow 模块的功能。因此,你可以将 another_module
的代码更改如下:
module another_module {
struct AssetManager has key {
asset: Referent<Asset>,
}
public fun get_asset(manager: &mut AssetManager): (Asset, Borrow) {
borrow::borrow(&mut manager.asset)
}
public fun return_asset(
manager: &mut AssetManager,
asset: Asset,
b: Borrow) {
borrow::put_back(&mut manager.asset, asset, b)
}
}
现在 PTB 可以检索资产,将其用于调用 use_asset
,然后返回资产。
注意事项
Borrow
对象是 borrow 模块提供的保证的关键。Borrow
的定义是
struct Borrow { ref: address, obj: ID }
这使得你无法在任何地方丢弃或保存它的实例,因此它必须在检索它的同一交易中被使用(烫手山芋)。此外,Borrow
结构中的字段确保返回的对象是同一个 Referent
,并且与最初由 Referent
实例持有的对象相同。换句话说,无法保留检索到的对象,也无法将其与位于不同 Referent
中的另一个对象交换。
使用 Referent
是一个非常显式和侵入性的更改。在设计解决方案时必须考虑到这一点。
计划在 PTB 中支持引用,这是 API 的一种更自然和适当的模式。
你必须考虑使用 borrow 模块的影响,以及是否有机制可以后续转移到更自然的引用模式。
最后,Referent
模型强制使用可变引用并返回一个对象。在设计 API 时,这两者都有重要的影响。你必须小心模块提供的逻辑以及对象的暴露方式。
示例
扩展前面的示例,调用 use_asset
的 PTB 如下所示:
// initialize the PTB
const txb = new TransactionBlock();
// load the assetManager
const assetManager = txb.object(assetManagerId);
// retrieve the asset
const [asset, borrow] = txb.moveCall({
target: "0xaddr1::another_module::get_asset",
arguments: [ assetManager ],
});
// use the asset
txb.moveCall({
target: "0xaddr2::a_module::use_asset",
arguments: [ asset ],
});
// return the asset
txb.moveCall({
target: "0xaddr1::another_module::return_asset",
arguments: [ assetManager, asset, borrow ],
});
...