Sui Move开发核心概念
本文旨在帮助其他 Solidity 和 OOP 开发人员快速掌握 Sui-Move 编程中使用的主要概念。特别是如果你像我一样必须在一周内掌握所有内容。
一键发币: SOL | BNB | ETH | BASE | Blast | ARB | OP | POLYGON | AVAX | FTM | OK
作为一名进入 Sui 世界的 Solidity 开发人员,之前没有使用过 Rust,第一次接触 Sui 时我有点不知所措。Sui 风格的 Move 不遵循当今最流行的高级语言中常见的面向对象编程 (OOP) 范式。Rust 概念也被重复使用,例如借用或类型,这些概念在 Sui 官方文档中没有明确解释。
此目标旨在帮助其他 Solidity 和 OOP 开发人员快速掌握 Sui-Move 编程中使用的主要概念。特别是如果你像我一样必须在一周内掌握所有内容。
1、Move对象
首先,让我们谈谈 Sui 的一个大力推广的功能:对象。这听起来可能令人困惑,因为对象是 OOP 语言中定义明确的类别,具有共同的特征。虽然两者都使用术语“对象”,但它们的实现和目的截然不同。是的,如果你问我,他们应该使用另一个名字。
在 Solidity 中,合约是 OOP 风格的对象,包含内部逻辑(代码)和数据(例如,代币余额图),并通过称为“地址”的单个 URI 引用。Sui 的行为并不像这样,它将逻辑和数据分开,每个都有自己的资源标识符。我的天哪!
因此,你在链上存储了两种类型的数据:
- 模块,包含代码/逻辑,但没有数据,就像你在常规合约中看到的那样。模块有点(但不完全)类似于 Solidity 中的库。它们不能自己存储数据。
- 对象,具有唯一标识并包含数据,但没有逻辑。也就是说,它们仍然链接到创建它们的模块,并管理它们的生命周期。
因此,编写 Move 代码包括构建“对象”和“模块”之间的关系。对象是一种类型化的数据,由模块创建,只能通过与其关联模块交互来更新或删除。两者都使用唯一的 32 字节地址进行引用(对对象进行了一些调整,我们稍后会看到)。
这种代码和数据分离的另一个重要结果是,与模块的任何交互都需要你提供所有必要的数据,因为模块不包含任何数据。 你无法从链上存储中提取数据,就像在 Solidity 中所做的那样(例如,检查另一个合约的状态) — 你需要在函数调用中直接提供这些数据。
等等,但如果我可以传递任何数据,嗯 - 我可以传递假数据,对吗?
为了避免这种情况,作为函数参数传递的对象带有一个引用,该引用在全局存储中标识它们,并且可以在执行期间检查其完整性。 此引用由两个名称指定:key 或 UID,它是对象地址的包装器。
这是一个对象的示例。 对象实际上就像一个经典的 json 对象,只是它包含自己的地址,类型为 UID:
struct DigitalArt has key {
id: UID, // This is where the key (aka UID) is stored
artist: address,
title: vector<u8>,
price: u64,
}
正如我们所见,对象可以通过其附带的模块创建和修改:
module digital_art::gallery {
// Define an object structure
struct DigitalArt has key, store {
id: UID, // Unique identifier for the object
artist: address,
title: vector<u8>,
price: u64,
}
// Function to create a new DigitalArt object
public fun create_art(
artist: address,
title: vector<u8>,
price: u64,
ctx: &mut TxContext
): DigitalArt {
DigitalArt {
// Generate a new UID for the object
id: object::new(ctx),
artist,
title,
price,
}
}
// Function to update the price of a DigitalArt object
public fun update_price(
art: &mut DigitalArt,
new_price: u64
) {
art.price = new_price;
}
}
在这里,修改价格的唯一方法是与模块交互。你可以在 update_price
中看到,我们将新价格与完整的对象数据一起传递,因为模块本身不包含任何数据。模块也无法从区块链中提取数据,就像我们在 Solidity 中所做的那样,只传递 UID。
2、对象所有权
但是等等,在这里,任何人都可以修改价格?你如何管理对模块中对象的访问?
这个问题通过引入另一个概念来解决:对象所有权。对象可以有两种所有权制度:
2.1 拥有的对象
由用户或另一个合约拥有,可以由其所有者直接转让,而无需与其引用模块交互。你只需使用 .transfer
函数即可。
拥有的对象只能由其记录的所有者在模块函数中传递。因此,在我们上面的例子中,只有所有者可以更新 DigitalArt 对象的价格。另一个试图这样做的用户将导致交易撤销。拥有的对象也可以存储在其他对象中,这允许有趣的用例,例如 defi 中的可组合性。
从深层次上讲,拥有对象的所有权制度与 UID 相关联。例如,你不能创建一个具有特定 UID 的新对象并期望将其转移回给你。
2.2 共享对象
共享对象也有自己的 UID,但任何人都可以将其用作公共函数的输入。这种对象非常适合常见数据,例如配置参数。由于它是一个对象,因此关联模块仍然控制其生命周期以及谁来更新它。
要共享一个对象,你需要对其调用 transfer::share_object(obj);
。
2.3 Move设计模式:基本管理
如果我们希望只有画廊所有者才能更新 DigitalArt 价格怎么办?我们可以通过在模块创建期间创建一个 GalleryOwner
对象并在 update_price
函数中要求它来实现这一点:
module digital_art::gallery {
// ...
// Define an object for admin functions
struct GalleryOwnerCap has key, store {
id: UID
}
// Equivalent to a constructor
fun init(ctx: &mut TxContext) {
let gallery_owner = GalleryOwnerCap {
id: object::new(ctx)
};
gallery_owner.transfer(tx_context::sender(ctx));
}
// ...
// Update the price of a DigitalArt object
public fun update_price(
_: &GalleryOwnerCap,
art: &mut DigitalArt,
new_price: u64
) {
art.price = new_price;
}
}
对于喜欢使用 Open Zeppelin 的 OnlyAdmin
装饰器的 solidity 开发人员来说,这就是实现相同效果的方法!
3、借用和改变
我们在上一节中看到,对象可以被拥有,但变量也可以!🤯 因此,让我们介绍变量所有权和借用的概念。当我发现它时,这对我来说相当新颖。事实上,在 Solidity 中,你可以传递变量,直接修改状态变量,而不必过多考虑谁在任何给定时间“拥有”特定数据。
Move 中没有这样的免费午餐,它强制执行严格的规则,规定谁在执行期间对被操纵的变量具有读写访问权限。这些限制是从 Rust 继承而来的,源于在允许并行化的同时需要安全执行。
3.1 理解变量所有权
在 Move 中,每个值都有一个所有者,他可以在给定时间操纵该值。当你将值分配给变量时,该变量将成为该值的所有者。如果你随后将该值移动到另一个变量或函数,则原始变量将失去所有权并变为无效。
这个概念很重要,你必须不断思考它,因为几乎每一行代码都包含对借用的引用。例如:
public struct MyStruct {
value: u64
}
let x = MyStruct { value: 42 }; // 'x' owns MyStruct
let y = x; // Ownership of MyStruct moves from 'x' to 'y'
// 'x' is now invalid and cannot be used 😱
如果你在将 x 的值移动到 y 后尝试使用 x,编译器将抛出错误,因为 x 不再拥有任何值。通过在定义结构时赋予其复制能力,可以复制结构。稍后会详细介绍!
3.2 移动与借用
在Move中,你可以移动(哈哈)一个值或借用它:
- 移动(moving)将值的所有权转移到另一个变量或函数。移动后,原始所有者将无法再使用该值。
- 借用(borrowing)会创建对值的引用而不转移所有权,允许你读取或修改该值,而原始所有者保留所有权。
等等!不要关闭页面!有两种类型的借用:
- 不可变借用:
&T
— 对类型 T 的值的只读引用。不可修改! - 可变借用:
&mut T
— 对类型 T 的值的读写引用。
以下是说明移动和借用的示例:
struct MyStruct { value: u64 }
fun example() {
let s = MyStruct { value: 42 }; // 's' owns the value
let s_ref = &s; // Immutable borrow of 's'
let s_mut_ref = &mut s; // Mutable borrow of 's'
let t = s; // Move ownership to 't'; 's' is now invalid
}
在此代码中,将 s 移动到 t 后,s 就不能再使用了。这确保在任何给定时间数据只有一个所有者。
为什么?这允许严格指定谁可以修改内存中的内容,从而防止数据争用和重入。鉴于 Sui 是并行化的,这是安全实现最高区块链速度的重要功能。
3.3 管理所有权
在 Move 中设计函数时,你需要决定函数是否应该拥有某个值的所有权、不可变地借用它还是可变地借用它:
- 取得所有权
fun foo(bar: T)
:函数使用该值,调用者失去所有权。 - 不可变借用
fun foo(bar: &T)
:函数可以读取该值而不对其进行修改,调用者保留所有权。 - 可变借用
fun foo(bar: &mut T)
:函数可以修改该值,调用者保留所有权。
让我们看看它是如何工作的:
取得所有权
假设我们有一个 DigitalArt 对象并想删除它:
public fun delete_art(art: DigitalArt) {
// 'art' is consumed here; caller loses ownership
let DigitalArt { id, artist, title, price } = art;
object::delete(id);
}
这里,函数获取了 art 的所有权。调用 delete_art
后,原所有者就不能再使用 art 了。
只读借用
如果我们想在不修改 DigitalArt 对象的情况下显示信息:
public fun display_art(art: &DigitalArt) {
// Immutable borrow; caller retains ownership
let artist = art.artist;
let title = art.title;
let price = art.price;
// Display or return the information
}
可修改借用
要更新 DigitalArt 的价格:
public fun update_price(art: &mut DigitalArt, new_price: u64) {
// Mutable borrow; caller retains ownership
art.price = new_price;
}
通过传递可变引用,我们可以在调用者仍拥有它时修改艺术品。
3.4 借用和所有权的神圣规则💫
Move 强制执行有关所有权和借用的严格规则以确保安全:
- 单一所有权:每个值在任何时间点都有一个所有者。
唯一可变访问:你可以对一个值拥有一个可变引用或多个不可变引用,但不能同时拥有两者。 - 引用不能比所有者存活更久:引用指向的数据被移动或销毁后,就不能使用。
记住这些!当你学习 Move 时,编译器会就代码的借用问题向你大喊大叫。
4、能力
在前面的例子中,你可能注意到提到了 key、store。这些是类型的能力:
struct GalleryOwnerCap has key, store {
id: UID
}
它们定义了对象可以做什么,可以做什么,不受过去的影响。
4.1 Key
Key
能力意味着类型可以在其字段中拥有 UID,在全局存储中拥有引用。这意味着在调用函数后数据可以保留。
问题是,如果你只有这种能力,则该对象不可转让。你可以将其转让给其所有者,但之后它就被灵魂绑定了。
module examples::key_only_object {
struct KeyOnlyObject has key {
id: UID
}
public fun create_key_only(ctx: &mut TxContext) {
let obj = KeyOnlyObject {
id: object::new(ctx)
};
// This is valid -
// the object will persist and be owned by the sender
transfer::transfer(obj, tx_context::sender(ctx));
}
4.2 Store
Store
能力解决了转移限制,允许所有者将其资产转移给其他用户。它还允许将对象嵌套到其他对象中。
module digital_art::gallery {
// Define an object structure
struct DigitalArt has key, store {
id: UID, // Unique identifier for the object
artist: address,
title: vector<u8>,
price: u64,
}
struct Gallery has key, store {
id: UID,
collection: vector<DigitalArt>
}
// ...
// Create a shared gallery object
public fun create_shared_gallery(ctx: &mut TxContext) {
let gallery = Gallery {
id: object::new(ctx),
collection: vector::empty(),
};
transfer::share_object(gallery);
}
// Add the art to the gallery's collection
public fun add_to_collection(
gallery: &mut Gallery,
art: DigitalArt
) {
vector::push_back(&mut gallery.collection, art);
}
}
具有存储但没有键的类型通常用于以灵活的方式构造将存储在对象内的数据。
4.3 Copy
Copy
能力允许复制值而不消耗原始值。等等,但“消耗”值到底是什么?
你可以通过执行以下操作在 Move 中使用值:
- 移动:转移非复制值的所有权
- 解包:将结构分解为其组件
- 按值传递:将非复制值传递给函数
因此,具有 Copy
能力的结构可以随意复制,这对于配置数据等内容来说非常方便。但如果我们谈论 NFT 或代币,则可能很危险。
让我们看看它在代码中是如何转换的:
struct MyCopyableStruct has copy { value: u64 }
let a = MyCopyableStruct { value: 10 };
let b = a; // 'a' is copied to 'b', 'a' is still valid
let c = a; // This is fine because 'a' wasn't consumed
struct NoCopy { value: u64 }
let a = NoCopy { value: 10 };
let b = a; // 'a' is consumed (moved) here
let c = a; // Error: 'a' has been consumed
fun take_nocopy(nc: NoCopy) { /* ... */ }
take_nocopy(b); // 'b' is consumed here
take_nocopy(b); // Error: 'b' has been consumed
let NoCopy { value } = b; // 'b' is consumed (unpacked) here
因此,具有 Copy
能力的值可以在你的代码中重复使用。这很棒!
为什么默认情况下不允许?
- 复制大型结构可能很昂贵,因此开发人员可能希望优化代码。
- 复制允许你潜在地对类似资产的对象进行双重支付!
为什么我们不能只使用借用?
你确实可以通过使用引用创建具有相同值的新结构来复制结构。这可能并不总是可行的,特别是如果所有结构字段都不可用。这也会导致代码复杂度增加(以及编译器的更多叫喊声)。
我们可以只复制具有 UID 的对象吗?然后会发生什么?
好问题!这似乎是双重支付的简单方法,对吧?答案是具有密钥功能的结构无法复制,因为唯一标识符不再是唯一的。因此密钥和复制不兼容。抱歉!
如果我想复制但不移动所有权怎么办?
编译器通常很聪明(比我聪明),能够推断出赋值是要复制还是要移动。你也可以尝试强制执行此行为,方法是编写 let y = copy x
或 let y. = move x
。
4.4 Drop
让我们来谈谈最后一个 - Drop
。 Drop
能力允许你丢弃或忽略它。如何丢弃类型?
如果类型没有键(因此没有 UID),你可以忽略它并将其留在这里。如果结构具有 UID 字段和可键性,你可以调用 object::delete(id);
。
struct MyObject has key {
id: UID
}
public fun delete_object(obj: MyObject) {
let MyObject { id } = obj;
object::delete(id);
}
4.5 Hot Potato
等等,但是如果一个类型没有能力怎么办?这有什么意义呢?在这种情况下,它被赋予了一个迷人的名字—— Hot Potato
(烫手山芋)。这样的结构必须被使用,不能被忽略。
这种模式通常用于闪电贷,它期望在同一笔交易中返还资金。鉴于 Sui Move 不像 Solidity 那样接受原始调用数据,因此你必须使用可编程交易块 (PTB)。PTB 类似于 flashbot 包,只是交易可以“链接”并接受先前交易的输出。它们作为一个单一交易执行。
在我们的上下文中,这意味着烫手山芋不必在创建它的同一函数中被使用!尽管如此,只有同一模块的函数才能使用它,从而产生了一个有趣的限制,我们可以将其用于闪电贷。
好的,举例说明!假设我们前面例子中的画廊老板想让其他用户从收藏中借用艺术品来制作个人副本。当然,艺术品需要立即归还,这就是我们要使用烫手山芋的原因。
module digital_art::gallery {
// Define the DigitalArt struct
struct DigitalArt has key, store {
id: UID, // Unique identifier for the object
artist: address,
title: vector<u8>,
price: u64,
}
// Define the Gallery struct
struct Gallery has key, store {
id: UID,
collection: vector<DigitalArt>,
}
// Our "hot potato" with no abilities
struct LoanedDigitalArt {
art: DigitalArt,
}
// Create a new DigitalArt object
public fun create_digital_art(
artist: address,
title: vector<u8>,
price: u64,
ctx: &mut TxContext,
): DigitalArt {
DigitalArt {
id: object::new(ctx),
artist,
title,
price,
}
}
// Create a new Gallery object
public fun create_gallery(ctx: &mut TxContext): Gallery {
Gallery {
id: object::new(ctx),
collection: vector::empty(),
}
}
// Add art to the gallery's collection
public fun add_to_collection(
gallery: &mut Gallery,
art: DigitalArt
) {
vector::push_back(&mut gallery.collection, art);
}
// Borrow digital art from the gallery
public fun borrow_digital_art(
gallery: &mut Gallery,
index: u64,
): LoanedDigitalArt {
let art = vector::remove(
&mut gallery.collection,
index
);
LoanedDigitalArt { art }
}
// Make a copy of the borrowed DigitalArt
public fun copy_digital_art(
loaned_art: &LoanedDigitalArt,
ctx: &mut TxContext,
): DigitalArt {
let original_art = &loaned_art.art;
DigitalArt {
id: object::new(ctx),
artist: original_art.artist,
title: original_art.title,
price: original_art.price,
}
}
// Return the borrowed digital art to the gallery
public fun return_digital_art(
gallery: &mut Gallery,
loaned_art: LoanedDigitalArt,
) {
let art = loaned_art.art;
vector::push_back(&mut gallery.collection, art);
}
}
我们测试:
#[test]
public fun test_flashloan_copy_and_return(ctx: &mut TxContext) {
let mut gallery = Gallery {
id: object::new(ctx),
collection: vector::empty(),
};
// Create and add a DigitalArt to the gallery
let art = create_digital_art(
@0x1,
b"Sunset Overdrive",
1000,
ctx
);
add_to_collection(&mut gallery, art);
// Borrow the DigitalArt from the gallery
let loaned_art = borrow_digital_art(&mut gallery, 0);
// Make a copy of the borrowed DigitalArt
let copied_art = copy_digital_art(&loaned_art, ctx);
// Return the borrowed DigitalArt to the gallery
return_digital_art(&mut gallery, loaned_art);
// At this point, the gallery has two artworks:
// - The original DigitalArt
// - The copied DigitalArt with a new UID but same content
}
}
阅读此代码时,你可能会问:数字艺术不应该在 copy_digital_art
函数的末尾返回吗?是的!但在这种情况下,我们只传递数字艺术的借用引用,因此无需返回原始对象。
如果你想知道 - 是的,你可以在 Move 中编写 Move 测试,就像使用 Foundry for Solidity 一样。
6、类型
Move 是一种类型化语言(对不起 Python 开发人员),从 Rust 继承了其语法和整体行为。现在,如果你不熟悉开发人员最受赞赏的语言™,那么类型系统可能会有点令人生畏,就像我开始学习 Move 时一样。
在 Move 中,有两种类型:原始类型和泛型。
- 原始类型是“经典”类型,例如 u64、bool、address 等。
- 泛型是提供的类型输入,以确保代码中的类型一致性,但未定义。
等等,你是什么意思没有定义?让我们解释一下。
6.1 泛型
如果你阅读了前面的部分,就会知道在 Sui Move 中,代码和数据是分开的。这就需要在函数调用期间传递所有必要的数据。因此,由于模块可能会接受非常不同的数据,因此有必要强制执行强大但灵活的类型系统,以确保整个事务执行过程中的类型一致性。
实际上,泛型允许你将嵌套类型作为函数的输入传递,并在代码中使用它们。例如,如果你有一个函数 bake
,将输入一些 Bread<BreadType>
并输出相同的 Bread<BreadType>
。
在这里,面包师必须输入 Bread
,但它可以是任何种类: Baguette
、 Bagel
、 Focaccia
等。由此产生的约束是他必须从烤箱中取出相同的 。
在函数的开头给出了泛型 BreadType
,以便编译器识别它是泛型: fun bake<GenericType>(...){}
。
module bakery::bread {
struct Bread<BreadType> has key, store {
id: UID,
baked: bool,
weight: u64,
bread_type: BreadType
}
// Different bread types
struct Baguette has drop {}
struct Bagel has drop {}
struct Focaccia has drop {}
// Generic function to create unbaked bread
public fun prepare<BreadType: drop>(
weight: u64,
bread_type: BreadType,
ctx: &mut TxContext)
: Bread<BreadType> {
return Bread<BreadType> {
id: object::new(ctx),
baked: false,
weight: weight,
bread_type: bread_type,
}
}
// Generic function to bake bread
public fun bake<BreadType: drop>(
bread: &mut Bread<BreadType>
): Bread<BreadType> {
bread.baked = true;
return bread
}
}
如你所见,泛型 <BreadType>
允许我们确保函数的输入和输出类型是一致的。
让我们测试一下:
#[test]
use std::type_name::{Self, TypeName};
fun test_baking() {
use sui::test_scenario;
// Start a test scenario
let scenario = test_scenario::begin(@0x1);
let ctx = test_scenario::ctx(&mut scenario);
let baguette = prepare<Baguette>(250, ctx);
bake(unbaked_baguette);
let type_name: TypeName =
type_name::get<Bread<Baguette>>();
let baguette_type_name: TypeName =
type_name::get<typeof(baguette)>();
// Compare the type names
assert!(type_name == baguette_type_name, 1);
}
在上面的代码中,你可能注意到 public fun bake<BreadType: drop>
。这是一个类型约束,要求类型输入具有删除能力。它为编译器增加了一层安全性,如果类型不具备正确的能力,编译器将不会执行事务。
6.2 幻像类型
当您定义一个结构时,尽管类型输入将被记录,但你无需在结构的字段中使用输入的类型。然后,该类型纯粹是信息性的,允许区分结构或强制约束而不影响结构的字段。
在这种情况下,你可以将 phantom
关键字添加到类型输入,以明确指定该类型未在字段中使用。请参阅上面的修改示例:
module bakery::bread {
// We define a phantom type here
struct Bread<phantom BreadType> has key, store {
id: UID,
baked: bool,
weight: u64,
// no "BreadType" field!
}
// Different bread types
struct Baguette has drop {}
struct Bagel has drop {}
struct Focaccia has drop {}
// Generic function to create unbaked bread
public fun prepare<BreadType: drop>(
weight: u64,
breadType: BreadType,
ctx: &mut TxContext)
: Bread<BreadType> {
return Bread<BreadType> {
id: object::new(ctx),
baked: false
weight: weight
}
}
// Generic function to bake bread
public fun bake<BreadType: drop>(
bread: &mut Bread<T>
): Bread<T> {
bread.baked = true;
return bread
}
}
我们从 Bread
结构中删除了 bread_type
字段。因此,现在类型输入可以具有 phantom
关键字。如果我们测试它, Bread
结构仍将是 <Bread<BreadType>>
类型!
请记住,这是可选的,但如果你不使用它,编译器会对您大喊大叫。事实上,它假设在结构的字段中应该使用非幻像类型,并且如果不使用,则期望幻像类型。
7、结束语
作为一名 Solidity 开发人员,深入研究 Sui Move 常常感觉像是进入了一个物理规则略有不同的平行空间。这很正常!我们只是触及了表面,因为还有许多更微妙的功能和设计模式需要讨论。总的来说,所有这些规则都应该允许你编写更安全的代码,并让链以最大速度处理交易。
应该从本文中记住什么?最重要的方面之一是 Sui Move 中代码和数据的分离。它迫使我们以不同的方式思考如何构建我们的程序。模块充当逻辑容器,而对象则承载状态,每个对象都有自己的标识符。
这种分离虽然最初令人困惑,但它鼓励在数据流和访问控制方面既模块化又清晰的设计。在后面的博客文章中,我们将讨论围绕它移动的可能设计模式。
最后,拥抱 Move 的范式可以让你成为一个更好的开发人员,不仅仅是在 Sui 方面,而且在理解安全智能合约开发的基本原则方面。所以慢慢来,尝试代码,不要害怕犯错——即使你的终端窗口充斥着错误,编译器也会原谅你,永远爱你。
谁知道呢?你可能会发现自己很欣赏 Move 代码的严格性!(不过我还没到那一步)
祝你编码愉快!
原文链接:A Gentle Introduction to Sui Move, for Solidity developers
DefiPlot翻译整理,转载请标明出处
免责声明:本站资源仅用于学习目的,也不应被视为投资建议,读者在采取任何行动之前应自行研究并对自己的决定承担全部责任。