本篇文章介绍 Move 编程语言的所有权
所有权是指一个资源只能被一个变量拥有,资源具有排他性,一个资源实例在同一时间只能有一个所有者。当一个变量拥有资源时,只有该变量可以对资源进行操作。所有权的转移通过赋值或函数参数传递来实现,一旦所有权被转移,原变量将失去对该资源的访问权。
下面代码中,create_resource
函数创建了一个 MyResource
实例并返回它,赋予调用者对该实例的所有权。transfer_ownership
函数接受一个 MyResource
实例和地址,从而将资源的所有权转移给指定地址,这个跟 Rust 有点不同,Move 要使用 transfer
模块来实现所有权的转移。
module hello::ownership_module {
use sui::object;
public struct MyResource has key {
id: UID,
value: u64,
}
public fun create_resource(value: u64, ctx: &mut TxContext): MyResource {
MyResource {
id: object::new(ctx),
value
}
}
public fun transfer_ownership(resource: MyResource, receipt: address) {
transfer::transfer(resource, receipt);
}
}
单一所有权:一个资源在任意时刻只能有一个所有者。通过赋值或函数参数传递所有权时,原所有者将失去对资源的访问权。防止双花:所有权模型有效地防止了资源被多次使用或重复消费的问题,这是区块链系统中防止双花攻击的关键。
引用则允许在不转移所有权的情况下访问资源。跟 Rust 一样,Move 支持不可变引用(&T)和可变引用(&mut T)。不可变引用允许读操作,而可变引用允许读和写操作。
下面是 Coin
模块中用于获取代币余额的两个函数,通过名字就可以发现,balance
函数是获取代币余额的不可变引用,而 balance_mut
则是获取可变引用,通常用于代币的一些可变操作,比如合并余额、拆分代币等。
module sui::coin {
public struct Coin<phantom T> has key, store {
id: UID,
balance: Balance<T>
}
/// Get immutable reference to the balance of a coin.
public fun balance<T>(coin: &Coin<T>): &Balance<T> {
&coin.balance
}
/// Get a mutable reference to the balance of a coin.
public fun balance_mut<T>(coin: &mut Coin<T>): &mut Balance<T> {
&mut coin.balance
}
}
生命周期管理:引用的生命周期受到严格控制,以确保引用在使用期间始终有效。这有效防止了悬挂引用和数据竞态。可变引用的独占性:在同一时刻,某个资源只能有一个可变引用。这确保了在修改资源时不会有其他引用对资源进行访问,保证了数据的一致性和安全性。
Move 中的泛型与 Rust 类似,可以使用类型参数来编写通用代码,通过尖括号(<>
)来指定类型参数。下面我们分别介绍几种范型的使用场景:
泛型函数:在 Move 中,可以使用泛型来定义函数,下面的函数接受两个相同类型的参数并返回它们交换后的结果。
module hello::generics {
public fun swap<T>(x: T, y: T): (T, T) {
(y, x)
}
}
泛型结构体: 在这个示例中,Pair 结构体包含两个不同类型的字段:T 和 U。
module hello::generics {
public struct Pair<T, U> has copy, drop {
first: T,
second: U,
}
public fun create_pair<T, U>(first: T, second: U): Pair<T, U> {
Pair { first, second }
}
}
phantom 泛型: 它是一种不实际使用类型参数的泛型。这种类型参数不会在结构体的字段或方法中出现,但它可以用来提供额外的类型信息或约束。
public struct Coin<phantom T> {
value: u64
}
这里的类型Coin
不包含任何使用类型参数 T
的字段或方法。它用于区分不同类型的 Coin
,并对类型参数 T
施加一些约束。
public struct USD {}
public struct EUR {}
#[test]
fun test_phantom_type() {
let coin1: Coin<USD> = Coin { value: 10 };
let coin2: Coin<EUR> = Coin { value: 20 };
// Unpacking is identical because the phantom type parameter is not used.
let Coin { value: _ } = coin1;
let Coin { value: _ } = coin2;
}
在上面的例子中,我们演示了如何使用不同的 phantom
类型参数USD
和EUR
来创建两个不同的Coin
实例。类型参数T不用于 Coin
类型的字段或方法,而是用于区分不同类型的 Coin
。它将确保USD
和EUR
代币不会混淆。
在使用泛型时,可以对类型参数施加约束。例如,可以限制类型参数必须实现某些能力,约束类型参数的语法是T: <ability> + <ability>
。
/// A generic type with a type parameter that has the `drop` ability.
public struct Droppable<T: drop> {
value: T,
}
/// A generic struct with a type parameter that has the `copy` and `drop` abilities.
public struct CopyableDroppable<T: copy + drop> {
value: T, // T must have the `copy` and `drop` abilities
}
Move Compiler
将强制要求类型参数T
具有指定的功能。如果类型参数不具备指定的功能,则代码不会编译。
/// Type without any abilities.
public struct NoAbilities {}
#[test]
fun test_constraints() {
// 报错,NoAbilities 的参数类型u64并没有drop能力
// let droppable = Droppable<NoAbilities> { value: 10 };
// 报错,NoAbilities 的参数类型u64并没有copy,drop能力
// let copyable_droppable = CopyableDroppable<NoAbilities> { value: 10 };
}
下面是 create_currency
函数的代码和解析,该函数用于创建一种新的货币类型,并返回该货币的 TreasuryCap
和 CoinMetadata
。它接受一个 phantom
类型的类型参数 T
,尽管 T
未在 TreasuryCap
和 CoinMetadata
的字段中使用,但它提供了类型区分和约束功能。同时确保 T
具有 drop
能力,这样编译器才能正确处理 T
的销毁。
public fun create_currency<T: drop>(
witness: T,
decimals: u8,
symbol: vector<u8>,
name: vector<u8>,
description: vector<u8>,
icon_url: Option<Url>,
ctx: &mut TxContext
): (TreasuryCap<T>, CoinMetadata<T>) {
// 确保类型 T 的唯一性(T 为一次性见证者)
assert!(sui::types::is_one_time_witness(&witness), EBadWitness);
(
TreasuryCap {
id: object::new(ctx),
total_supply: balance::create_supply(witness)
},
CoinMetadata {
id: object::new(ctx),
decimals,
name: string::utf8(name),
symbol: ascii::string(symbol),
description: string::utf8(description),
icon_url
}
)
}
在 Move 编程语言中,type_name
模块提供了将 Move 类型转换为其字符串表示形式的功能。这个模块对于调试和类型检查尤为重要,允许开发者在运行时获取类型的详细信息。本文将详细介绍 type_name
模块的功能、各个函数的作用以及实际应用场景。
TypeName
结构体表示一个类型名,其定义如下,该结构体包含一个字段 name
,用于存储类型的字符串表示,例如:
0000000000000000000000000000000000000000000000000000000000000000::your_module::your_type
public struct TypeName has copy, drop, store {
name: String
}
获取类型的值表示: 此函数返回类型 T 的 TypeName
表示。TypeName
包含类型的完整名称,包括包名和类型名。它是一个原生函数,其实现依赖于 Move 虚拟机的内部机制,用于在运行时获取类型 T 的字符串表示。
public native fun get<T>(): TypeName;
判断是否为基础类型: 该函数用于检查 TypeName
是否为基础类型,如 u8、u64、bool
等。它通过比较 TypeName
的字符串表示与基础类型的字符串来判断类型是否为基础类型。
public fun is_primitive(self: &TypeName): bool {
let bytes = self.name.as_bytes();
bytes == &b"bool" ||
bytes == &b"u8" ||
bytes == &b"u16" ||
bytes == &b"u32" ||
bytes == &b"u64" ||
bytes == &b"u128" ||
bytes == &b"u256" ||
bytes == &b"address" ||
(bytes.length() >= 6 &&
bytes[0] == ASCII_V &&
bytes[1] == ASCII_E &&
bytes[2] == ASCII_C &&
bytes[3] == ASCII_T &&
bytes[4] == ASCII_O &&
bytes[5] == ASCII_R)
}
获取字符串表示: 返回 TypeName
的 name
字符串引用。
public fun borrow_string(self: &TypeName): &String {
&self.name
}
获取地址字符串: 此函数提取并返回 TypeName
的地址部分,前提是该类型不是基础类型。它的实现逻辑也非常简单:通过字符串处理提取地址部分,该地址部分位于字符串的前面部分,长度为地址长度的两倍(因为是16进制表示)。我们以 SUI 代币的 TypeName
0000000000000000000000000000000000000000000000000000000000000002::sui::SUI
为例,该函数返回值为前面的地址部分,即 000…002
。
注意事项:此函数仅适用于非基础类型,使用前需检查类型是否为基础类型。
public fun get_address(self: &TypeName): String {
assert!(!self.is_primitive(), ENonModuleType);
let len = address::length() * 2;
let str_bytes = self.name.as_bytes();
let mut addr_bytes = vector[];
let mut i = 0;
while (i < len) {
addr_bytes.push_back(str_bytes[i]);
i = i + 1;
};
ascii::string(addr_bytes)
}
获取模块名: 该函数提取并返回 TypeName
的模块名部分。通过遍历字符串在地址部分后面的部分,直到遇到双冒号 ::
,提取模块名称。以 SUI 代币的 TypeName
为例,这里返回的是 sui
字符串
public fun get_module(self: &TypeName): String {
assert!(!self.is_primitive(), ENonModuleType);
let mut i = address::length() * 2 + 2;
let str_bytes = self.name.as_bytes();
let mut module_name = vector[];
loop {
let char = &str_bytes[i];
if (char != &ASCII_COLON) {
module_name.push_back(*char);
i = i + 1;
} else {
break
}
};
ascii::string(module_name)
}
右侧展示了如何使用 type_name
模块来获取模块中的指定结构体的信息。这里分别使用 borrow_string
函数获取结构体 hello
的 TypeName
,通过 get_module
函数获取模块信息,通过 get_address
获取地址信息,因为这里并没有进行链上部署,因此获取的是默认的零地址,执行结果如下:
module hello::type_reflection {
use std::ascii::String;
use std::type_name::{Self, TypeName};
use std::debug;
/// A function that returns the name of the type `T` and its module and address.
public fun do_i_know_you<T>(): (String, String, String) {
let type_name: TypeName = type_name::get<T>();
// there's a way to borrow
let str: &String = type_name.borrow_string();
let module_name: String = type_name.get_module();
let address_str: String = type_name.get_address();
// and a way to consume the value
let str = type_name.into_string();
(str, module_name, address_str)
}
#[test_only]
public struct hello {}
#[test]
fun test_type_reflection() {
let (type_name, module_name, _address_str) = do_i_know_you<hello>();
debug::print(&type_name);
debug::print(&module_name);
debug::print(&_address_str);
//
assert!(module_name == b"type_reflection".to_ascii_string(), 1);
}
}
执行代码:
> sui move test test_type_reflection
Running Move unit tests
[debug] "0000000000000000000000000000000000000000000000000000000000000000::type_reflection::hello"
[debug] "type_reflection"
[debug] "0000000000000000000000000000000000000000000000000000000000000000"
[ PASS ] 0x0::type_reflection::test_type_reflection
Test result: OK. Total tests: 1; passed: 1; failed: 0
Move 单元测试主要使用了以下三个注解:
#[test]
将一个函数标记为测试用例;#[expected_failure]
标记一个期望失败的测试用例;#[test_only]
将模块或模块成员(引用、函数、结构体或常量)标记为仅在测试时包含。任何带有 #[test_only]
或 #[test]
注解的模块或模块成员在编译时都不会包含在编译的字节码中,这样使我们部署在链上的代码更干净。
#[test] 注解
只能放在不带参数的函数上,它将该函数标记为要由单元测试框架运行的测试用例。一个简单的例子:
#[test]
fun test_example() {
// Test case logic
}
#[expected_failure] 注解
可以与 #[test]
结合使用,标记一个期望会失败的测试用例。有几种期望失败的情况,比如期望特定的中止码、算术错误、向量错误等,详细说明见后文。下面的代码中,coin
对象包含的字段值为 0,因此在调用 make_sure_non_zero_coin
函数时,断言失败,返回 ECoinIsZero
错误,被 #[expected_failure]
捕获到,也就得出了预期的错误结果。
public struct Wrapper(u64)
const ECoinIsZero: u64 = 0;
public fun make_sure_non_zero_coin(coin: Wrapper): Wrapper {
assert!(coin.0 > 0, ECoinIsZero);
coin
}
#[test]
#[expected_failure(abort_code = ECoinIsZero)]
fun make_sure_zero_coin_fails() {
let coin = Wrapper(0);
let _ = make_sure_non_zero_coin(coin);
}
#[test_only] 注解
可以将模块及其成员标记为仅在测试时包含。被标记为 #[test_only]
的项在非测试模式下编译时将被排除。注意,#[test_only]
函数本身不是测试用例,而只能在测试代码中被调用。
#[test_only]
2module my_module { /* ... */ }
3
4#[test_only]
5const MY_CONST: u64 = 42;
6
7#[test_only]
8fun helper_func() { /* ... */ }
当测试代码编写完成后,执行 sui move test
进行测试
一个 Move 交易可能成功执行或者失败中止。成功执行时,所有对对象和链上数据的更改都会被应用并提交到区块链上。而如果交易中止了,之前所有的更改都会被恢复到交易开始前的状态。
abort
关键字用于主动中止交易的执行。当遇到不可恢复的错误时,开发者可以使用 abort
语句终止交易并返回一个错误码。错误码是一个 u64
类型的整数值,可以是任意值。比如在验证用户访问权限的时候,如果用户没有权限访问某个功能,我们可以这样写,当用户无权限时,就会以错误码 0
中止交易。
let user_has_access = true;
if (!user_has_access) {
abort 0;
}
相比手动使用 abort
,assert!
宏提供了一种更简便的方式来检查条件并中止交易。assert!
接受两个参数:一个布尔条件和一个错误码。如果条件为 false
,交易会以给定的错误码中止。
assert!(user_has_access, 0);
为了让错误信息更加明确和有意义,通常会定义一些常量来表示不同的错误类型。这些常量通常以 E 开头并采用驼峰命名法,比如 ENoAccess
、ENoField
等。
const ENoAccess: u64 = 0;
2const ENoField: u64 = 1;
3
4public fun update_record(user_has_access: bool, field_exists: bool) {
5 assert!(user_has_access, ENoAccess);
6 assert!(field_exists, ENoField);
7 // ...
8}
在这个例子中,我们定义了两个错误常量来表示"没有访问权限"和"字段不存在"这两种错误情况。在 update_record
函数中,我们使用 assert!
来检查这两个条件,如果条件不满足就会以对应的错误码中止交易。这样不仅提高了代码的可读性,也使得错误处理更加清晰和友好。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!