五、Move 所有权

  • Ch1hiro
  • 发布于 1天前
  • 阅读 119

本篇文章介绍 Move 编程语言的所有权

1、所有权和作用域

1.1、所有权

​ 所有权是指一个资源只能被一个变量拥有,资源具有排他性,一个资源实例在同一时间只能有一个所有者。当一个变量拥有资源时,只有该变量可以对资源进行操作。所有权的转移通过赋值或函数参数传递来实现,一旦所有权被转移,原变量将失去对该资源的访问权。

​ 下面代码中,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);
    }
}

单一所有权:一个资源在任意时刻只能有一个所有者。通过赋值或函数参数传递所有权时,原所有者将失去对资源的访问权。防止双花:所有权模型有效地防止了资源被多次使用或重复消费的问题,这是区块链系统中防止双花攻击的关键。

1.2、引用

​ 引用则允许在不转移所有权的情况下访问资源。跟 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
    }
}

生命周期管理:引用的生命周期受到严格控制,以确保引用在使用期间始终有效。这有效防止了悬挂引用和数据竞态。可变引用的独占性:在同一时刻,某个资源只能有一个可变引用。这确保了在修改资源时不会有其他引用对资源进行访问,保证了数据的一致性和安全性。

2、泛型

2.1、概念介绍

Move 中的泛型与 Rust 类似,可以使用类型参数来编写通用代码,通过尖括号(<>)来指定类型参数。下面我们分别介绍几种范型的使用场景:

泛型函数:在 Move 中,可以使用泛型来定义函数,下面的函数接受两个相同类型的参数并返回它们交换后的结果。

module hello::generics {
    public fun swap<T>(x: T, y: T): (T, T) {
        (y, x)
    }
}

泛型结构体: 在这个示例中,Pair 结构体包含两个不同类型的字段:TU

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 类型参数USDEUR来创建两个不同的Coin实例。类型参数T不用于 Coin 类型的字段或方法,而是用于区分不同类型的 Coin。它将确保USDEUR 代币不会混淆。

2.2、类型约束

在使用泛型时,可以对类型参数施加约束。例如,可以限制类型参数必须实现某些能力,约束类型参数的语法是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 };
}

2.3、示例代码

下面是 create_currency 函数的代码和解析,该函数用于创建一种新的货币类型,并返回该货币的 TreasuryCapCoinMetadata。它接受一个 phantom 类型的类型参数 T,尽管 T 未在 TreasuryCapCoinMetadata 的字段中使用,但它提供了类型区分和约束功能。同时确保 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
        }
    )
}

3、获取泛型结构体名称

3.1、获取泛型结构体名称

在 Move 编程语言中,type_name 模块提供了将 Move 类型转换为其字符串表示形式的功能。这个模块对于调试和类型检查尤为重要,允许开发者在运行时获取类型的详细信息。本文将详细介绍 type_name 模块的功能、各个函数的作用以及实际应用场景。

3.2、结构体定义

TypeName 结构体表示一个类型名,其定义如下,该结构体包含一个字段 name,用于存储类型的字符串表示,例如:

0000000000000000000000000000000000000000000000000000000000000000::your_module::your_type

public struct TypeName has copy, drop, store {
    name: String
} 

3.3、核心函数

获取类型的值表示: 此函数返回类型 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)
}

获取字符串表示: 返回 TypeNamename 字符串引用。

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)
}

3.4、代码示例

右侧展示了如何使用 type_name 模块来获取模块中的指定结构体的信息。这里分别使用 borrow_string 函数获取结构体 helloTypeName,通过 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

4、单元测试

4.1、Unit Test

Move 单元测试主要使用了以下三个注解:

  • #[test] 将一个函数标记为测试用例;
  • #[expected_failure] 标记一个期望失败的测试用例;
  • #[test_only] 将模块或模块成员(引用、函数、结构体或常量)标记为仅在测试时包含。

任何带有 #[test_only]#[test] 注解的模块或模块成员在编译时都不会包含在编译的字节码中,这样使我们部署在链上的代码更干净。

4.2、语法

#[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 进行测试

5、断言和中断 Assert & Abort

5.1、语法

​ 一个 Move 交易可能成功执行或者失败中止。成功执行时,所有对对象和链上数据的更改都会被应用并提交到区块链上。而如果交易中止了,之前所有的更改都会被恢复到交易开始前的状态。

abort 关键字用于主动中止交易的执行。当遇到不可恢复的错误时,开发者可以使用 abort 语句终止交易并返回一个错误码。错误码是一个 u64 类型的整数值,可以是任意值。比如在验证用户访问权限的时候,如果用户没有权限访问某个功能,我们可以这样写,当用户无权限时,就会以错误码 0 中止交易。

let user_has_access = true;
if (!user_has_access) {
    abort 0;
}

相比手动使用 abortassert! 宏提供了一种更简便的方式来检查条件并中止交易。assert! 接受两个参数:一个布尔条件和一个错误码。如果条件为 false,交易会以给定的错误码中止。

assert!(user_has_access, 0);

为了让错误信息更加明确和有意义,通常会定义一些常量来表示不同的错误类型。这些常量通常以 E 开头并采用驼峰命名法,比如 ENoAccessENoField 等。

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! 来检查这两个条件,如果条件不满足就会以对应的错误码中止交易。这样不仅提高了代码的可读性,也使得错误处理更加清晰和友好。

点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
Ch1hiro
Ch1hiro
一名Web3初学者