二、Move 基础类型

  • Ch1hiro
  • 更新于 2024-11-15 11:31
  • 阅读 294

Move 的基础类型

Sui_logo.png

1、原生类型 Booleans

1.1、原生类型 Booleans

​ Move 有许多内置的原生类型,它们是构成其他类型的基础。原生类型包括:布尔值(Booleans)、无符号整数(Unsigned Integers)和地址(Address)。本节我们介绍布尔类型,其他的会在后面的章节介绍。

​ Booleans 类型表示一个布尔值,它有两个可能的值:true和 false,布尔类型非常简单,通常用于存储标志并控制程序流程。

1.2、Documentation

​ 在 Move 中声明和分配变量的语法跟 Rust 一致,使用 let 关键字声明变量,默认情况下变量是不可变的 immutable,但可以使用关键字 let mut 使其可变。

1let <variable_name>[: <type>]  = <expression>;
2let mut <variable_name>[: <type>] = <expression>;

​ 其中 <variable_name> 为变量的名称,<type> 为变量的类型(可选),<expression> 为要分配给变量的值。如下为布尔类型的赋值:

1let x: bool = true;
2let mut y: bool = false;

对于布尔值,无需显式指定类型,因为编译器可以从值中推断出类型。因此可以简写成如下的方式:

1let x = true;
2let mut y = false;

2、原生类型 Integers

2.1、原生类型 Integers

​ Move 支持 6 种无符号整数类型(u8, u16, u32, u64, u128, u256),这些类型的值范围从 0 到最大值,具体取决于类型的大小。与 Rust 不同的是,Move 中没有负数和小数。将数值类型细分的目的是减少资源占用和性能优化。

2.2、Documentation

数值的表示可以使用十进制或十六进制形式:

1// 使用
2let x: u8 = 10;
3
4// 使用十六进制表示数值
5let y: u8 = 0xa;

在定义数值变量时,我们可以显式指定其类型(后缀或者直接指定类型),如果未指定,编译器将尝试从上下文中推断,如果无法推断类型,则假定为 u64 类型。

1// 使用后缀表示其类型
2let explicit_u8 = 1u8;
3
4// 直接指定变量类型
5let simple_u8: u8 = 1;
6
7// 通过下划线提高可读性
8let explicit_u256_underscored = 123_456_789_123u256;
9let hex_u256: u256 = 0x1123_456A_BCDE_F;

2.3、FAQ

  • Q1 :MOVE 中数值的算数运算有什么特别的地方?

​ A:对于这些类型的算数操作(加减乘除求余),两个参数(左侧和右侧操作数)必须具有相同的类型。如果需要对不同类型的值进行操作,则需要首先执行强制转换

当发生溢出或被零除的情况时,所有算术运算都会中止,而不是以上溢、下溢、被零除等数学整数未定义的的方式输出结果,可以有效避免溢出攻击,提高程序等安全性。

  • Q2 :MOVE 中如何进行数值转换 Casting?

​ A:MOVE 中使用 as 关键字进行类型的转化,使得一种大小的整数类型可以转换为另一种大小的整数类型。如果结果对于指定类型来说太大,则转换将中止(例如向下转换,把一个 u16 的值转为 u8,则很有可能会发生溢出)。

整数是 Move 中唯一支持强制转换的类型,下面我们把 y 转换成 u64 类型后,就可以用于计算 _z 的值了。

public fun casting_fun() {
2    // x is a u8
3    let x: u8 = 123; 
4    // y is a u64 with the same value as x
5    let y: u64 = x as u64; 
6    
7    let _z = 112u64 + y;
8}

3、原生类型 Address

3.1、原生类型 Address

在 Move 中,地址 Address 是一种内置的基本类型,用来表示区块链上的位置或账户。一个地址值是一个 256 位( 32 字节)的标识符。通常表示为前缀为 0x 的十六进制字符串,地址不区分大小写。

0xc494732d09de23389dbe99cb2f979965940a633cf50d55caa80ed9e4fc4e521e

上面的地址是有效地址的示例。它的长度为 64 个字符(32 个字节),并以0x开头。

地址类型是 Move 语言中不可或缺的元素,它用于标识以下实体:

模块包: 每个模块包都有一个唯一的地址,用于区分不同的模块集合。

账户: Sui 区块链上的每个账户都拥有一个地址,用于存储和管理资产。

对象: Move 语言中,对象是存储在区块链上的数据结构,每个对象也拥有一个地址。

交易发送: 交易的发送和接收都需要指定目标地址。

3.2、Documentation

​ Move 语言中的 Address 可以是数值地址(以 0x 开头),也可以是命名地址(在 Move.toml 文件中进行注册),地址通常以 @ 符号开头,后跟数值或命名标识符。

​ 命名地址的语法遵循 Move 中任何命名标识符的相同规则。数字地址的语法不限于十六进制编码值,任何有效的 u256 数字值都可以用作地址值,例如 42、0xCAFE 和 10_000 都是有效的数字地址文字。

​ 编译时,十六进制数被解释为 32 字节值,对于命名地址,编译器会在 Move.toml 文件中查找标识符并替换为相应的地址。如果在 Move.toml 文件中找不到标识符,编译器将抛出错误。

3.3、FAQ

Sui 中有哪些预留地址?

​ 预留地址是在 Sui 上有特定用途的特殊地址,它们在环境之间保持不变。预留地址通常是易于记忆和输入的简单值。例如,标准库 @std 的地址是 0x1。十六进制的表示方式如下,小于 32 字节的地址在左侧用零填充。

以下是保留地址的一些示例:

  • 0x1:MOVE 标准库 std ,是 MOVE 语言的工具包。
  • 0x2:Sui 框架 sui ,是 SUI 区块链场景中常用的一些工具包。
  • 0x5SuiSystem对象的地址
  • 0x6Clock对象的地址,该对象存储自 Unix 纪元以来的当前
  • 0x8Random 对象的地址。

3.4、地址类型转换

Sui Framework 提供了一组辅助函数来处理地址。由于地址类型是 32 字节值,因此可以将其转换为类型 u256,反之亦然。它还可以在 vector<u8>String 类型之间进行转换,方便进行数据存储和传输。

示例代码:

module hello::hello_module {
    use sui::address;
    use std::string::String;

    public fun address_fun2() {
        // 将地址转换为 u256 类型
        let addr_as_u256: u256 = address::to_u256(@0x1);
        // print: 1
        debug::print(&addr_as_u256);

        // 将 u256 类型转换为地址
        let addr = address::from_u256(addr_as_u256);
        // print: @0x1
        debug::print(&addr);

        // 将地址转换为 vector&lt;u8> 类型
        let addr_as_u8: vector&lt;u8> = address::to_bytes(@0x1);
        // print: 0x0000000000000000000000000000000000000000000000000000000000000001
        debug::print(&addr_as_u8);

        // 将 vector&lt;u8> 类型转换为地址
        let addr = address::from_bytes(addr_as_u8);
        // print: @0x1
        debug::print(&addr);

        // 将地址转换为字符串
        let addr_as_string: String = address::to_string(@0x1);
        // print: "0000000000000000000000000000000000000000000000000000000000000001"
        debug::print(&addr_as_string);
    }
}

4、结构体类型 Struct

4.1、结构体类型 Struct

Struct 是最常用的自定义数据类型之一。Move 语言使用 Struct 来表示一组相关数据的集合,它类似于其他编程语言中的结构体或对象。本文将深入介绍 StructMove 中的用法和特性。

4.2、结构体的定义

​ 在 Move 中,结构体的定义使用 struct 关键字,前面需要加 public 修饰符,后面跟上结构体的名称和字段列表。每个字段都需要指定名称和类型,并用逗号分隔。默认情况下,Struct 及其字段都是私有的,只能在定义模块内部访问。

// syntax
public struct StructName {
    field_name: field_type,*
}

// example
public struct Student {
    id: u32,
    name: String,
    score: u8,
}

需要注意的是,Move 不支持递归定义的 Struct,即一个 Struct 不能包含自身类型的字段。因此下面的定义是无效的。

public struct Foo { x: Foo }
//                     ^ ERROR! recursive definition

public struct A { b: B }
public struct B { a: A }
//                   ^ ERROR! recursive definition

public struct D(D)
//              ^ ERROR! recursive definition

4.3、创建和使用Struct实例

定义完 Struct 后,就可以创建该类型的实例了。通过如下的语法进行实例化,所有字段都必须赋值。

// syntax
let struct_obj = struct_name {
    field1: value1,*
}

// example
let stu = Student {
    id: 1u16,
    name: string::utf8(b"Alice"),
    score: 99u8,
};

要访问 Struct 字段的值,使用 . 操作符加字段名。如果在定义模块之外访问,编译器会报错,因为字段默认是私有的。

//只能在定义模块内访问
let name = stu.name;

4.4、FAQ

  • Move 语言中的结构体如何进行解构和销毁操作?

​ MOVE 语言中的解构和销毁操作对于构建健壮、安全的区块链应用程序至关重要。MOVE 是面向对象(资产)的编程,所有创建的资产必须要有一个归属,要么分配给某个所有者,要么用完就必须销毁,这有助于确保数字资产的所有权管理、状态的一致性,以及整体系统的可靠性和性能。

  1. 解构 (Destructuring)是一种将结构体实例拆分成单独变量的方法。通过模式匹配的方式,可以将结构体的各个字段分别赋值给新的变量。

    public struct Foo { x: u64, y: bool }
    
    fun example() {
       let foo = Foo { x: 3, y: true };
    
       // 使用模式匹配解构 foo
       let Foo { x, y } = foo;
       // 现在有两个新的局部变量:
       // x: u64 = 3
       // y: bool = true
    }
    // 我们将 foo 结构体实例解构为两个新变量 x 和 y。这样就可以独立地访问和操作结构体的各个字段了。
  2. 销毁 (Destroy),当结构体的生命周期结束时,它必须被销毁。这需要用到 drop 能力,该能力允许结构体实例被安全地删除。如果结构体没有 drop 能力,那么当它离开作用域时编译器会报错,因为它不知道如何安全地销毁这个值。

    public struct Foo { x: u64 }
    
    fun example() {
       let foo = Foo { x: 3 };
       // 使用解构语法销毁 foo
       let Foo { x } = foo;
       // 现在 x 变量可以被使用,foo 已经被安全地销毁了
    }
    
    // 通过解构语法将 foo 结构体的 x 字段值取出,然后 foo 本身就被安全地销毁了

5、动态数组 Vector

5.1、动态数组 Vector

Vector 是 Move 编程语言中唯一的原生集合类型(位于 std::vector 模块中)。vector<T> 是类型为 T 的同构集合,只能存储相同类型的元素,可以通过从末端推入 push、弹出 pop 操作动态的增加或删除元素。

Vector 在 Move 语言中的应用非常广泛,尤其是在处理数据和构建链上程序时扮演着关键作用。下面我们就来系统地了解一下 Vector 在 Move 语言中的语法、特性和使用方式。

Vector 的类型定义使用 vector 关键字(注意这里是小写),后面加上尖括号<T>中指定元素的类型,其中 T 可以是任意类型。它的类型定义及实例化如下:

// 定义一个空的布尔类型的 Vector
let empty: vector&lt;bool> = vector[];  

// 定义一个包含 u8 类型元素的 Vector
let v: vector&lt;u8> = vector[10, 20, 30];

// 定义一个包含 vector&lt;u8> 类型元素的Vector
let vv: vector&lt;vector&lt;u8>> = vector[vector[10, 20], vector[30, 40]];

5.2、Vector的基本操作

Move 标准库在 std::vector 模块中提供了各种操作 Vector 的函数:

  1. vector::push_back&lt;T>(v: &mut vector&lt;T>, t: T) 在 Vector 末尾添加元素。
  2. vector::pop_back&lt;T>(v: &mut vector&lt;T>) T: 移除 Vector 的最后一个元素。
  3. vector::length&lt;T>(v: &vector&lt;T>): u64 返回 Vector 的长度。
  4. vector::empty&lt;T>(): vector&lt;T> 创建一个空向量
  5. vector::is_empty&lt;Element>(v: &vector&lt;T>): bool 判断 Vector 是否为空。
  6. vector::remove&lt;T>(v: &mut vector&lt;T>, i: u64) T: 移除 Vector 指定索引位置的元素。
  7. vector::singleton&lt;T>(e: T): vector&lt;T> 创建一个包含 T 的大小为 1 的 Vector。
  8. vector::contains&lt;T>(v: &vector&lt;T>, e: &T): bool 判断 Vector 中是否包含指定元素。
  9. vector::borrow&lt;T>(v: &vector&lt;T>, i: u64): &T 获取对象向量 v 的第 i 个元素的不可变引用,如果超出范围则终止
  10. vector::swap&lt;T>(v : &mut vector&lt;T>, i: u64, j: u64)交换向量 v 中第 i 个和第 j 个索引处的元素,如果超出范围则终止
  11. vector::index_of&lt;T>(v: &vector&lt;T>, e: &T): (bool, u64) 如果 e 位于向量 v 的索引 i 处,则返回 ( true , i)。否则,返回 ( false , 0)
let mut v = vector[10u8, 20, 30];

assert!(v.length() == 3, 0);
assert!(!v.is_empty(), 1);

// 追加元素
v.push_back(40);
// 移除最后一个元素
let last_value = v.pop_back();

assert!(last_value == 40, 2);

let only_one = vector::singleton&lt;u64>(10);
assert!(only_one.length() == 1, 3);

示例代码:

module hello::hello_module {
    public struct MyStruct has copy, drop { value: u64 }

        #[test]
        public fun vector_fun() {
            // 创建空 Vector
            let empty = vector::empty&lt;MyStruct>();
            assert!(vector::is_empty(&empty), 0);

            // 创建包含 2 个元素的 Vector
            let mut v = vector[MyStruct { value: 1 }, MyStruct { value: 2 }]; 
            assert!(vector::length(&v) == 2, 1);

            // 访问元素
            assert!(*vector::borrow(&v, 0) == MyStruct { value: 1 }, 2); 

            // 添加元素
            vector::push_back(&mut v, MyStruct { value: 3 });
            assert!(vector::length(&v) == 3, 3);

            // 移除元素
            let last = vector::pop_back(&mut v); 
            assert!(last == MyStruct { value: 3 }, 4);
            // 这个函数的返回是向量中被移除的元素

            // 交换元素位置
            vector::swap(&mut v, 0, 1);

            // 包含判断
            assert!(vector::contains(&v, &MyStruct { value: 2 }), 5);

            // 索引查找
            let (ok, idx) = vector::index_of(&v, &MyStruct { value: 1 });
            assert!(ok && idx == 1, 6);

            // 删除指定索引元素
            vector::remove(&mut v, 0);
            assert!(vector::length(&v) == 1, 7);
        }
}

5.3、FAQ

  • 什么是 vector<u8> 字节数组字面量?

​ Move 中 Vector 的一个常见用例是用 vector&lt;u8> 表示的“字节数组”。这些字节数组非常常见,但它们的可读性非常差,所以 Move 提供了以下两种语法来提升其可读性:字节字符串字面量(Byte Strings Literals)十六进制字符串字面量(Hex Strings Literals

  • 字节字符串字面量,它使用 b 前缀来标记,如b"Hello!\n"。这些是ASCII编码的字符串,支持转义序列如\n\r\t等,每个字符对应一个字节。
  • 十六进制字符串字面量,它使用 x 前缀来标记,如 x"48656C6C6F210A",每两个十六进制数字代表一个字节。
fun test_string_literals() {
    assert!(b"" == x"", 0); 
    assert!(b"Hello!\n" == x"48656C6C6F210A", 1);
    assert!(b"\x48\x65\x6C\x6C\x6F\x21\x0A" == x"48656C6C6F210A", 2);
}

6、可选值 Option

Option 是 Move 语言中一个非常重要的类型,它用于表示可能存在或不存在的值。Option 的概念借鉴自 Rust 语言 ​ 通过使用 Option 可以优雅地处理可能为空的值,避免空指针异常,因为 Option 始终是一个有效的值,即使它不包含任何值,同时也能提高代码可读性,使代码更易于理解,因为它明确表示了某个值可能不存在。 ​ Option 是 Move 标准库中定义的一个通用类型,位于 std::option路径下,它会默认导入到我们的模块中,因此不需要显式引入,其定义如下:

/// 表示可能存在或不存在的值的抽象。
struct Option&lt;Element> has copy, drop, store {
    vec: vector&lt;Element>
}

public fun is_none&lt;Element>(t: &Option&lt;Element>): bool {
    t.vec.is_empty()
}

​ 从定义可以看出,Option 是一个参数化类型,它包含了一个 vector 类型的字段 vec。当 vec 的长度为 0 时,表示 Option 中不存在值,即 None 状态;当 vec 的长度为 1 时,表示 Option 中存在一个值,即 Some 状态。 ​ Move 标准库为 Option 提供了丰富的操作方法,包括创建 SomeNone 实例、判断状态、提取值等,可以非常方便地使用它们。

// Return an `Option` containing `e`
public fun some&lt;Element>(e: Element): Option&lt;Element> {
    Option { vec: vector::singleton(e) }
}

public fun none&lt;Element>(): Option&lt;Element> {
    Option { vec: vector::empty() }
}

创建 Option 实例的示例:

// 创建一个包含值 "Alice" 的 Option
let some_value: Option&lt;vector&lt;u8>> = option::some(b"Alice");

// 创建一个不包含值的 Option
let none_value: Option&lt;vector&lt;u8>> = option::none();

7、字符串 String

7.1、字符串 String

​ Move 语言本身并不提供内置的字符串类型,但它在标准库中提供了 string 模块来处理字符串数据。该模块定义了表示 UTF-8 编码的字符串类型。此外,标准库还提供了另一个模块 ascii,它定义了只包含 ASCII 字符的字符串类型。

​ Move 标准库中的字符串模块,它在底层是一个 vector&lt;u8> 集合类型,每个元素占用一个或多个字节。因此从本质上讲,Move 中的字符串就是一个字节数组,只不过它附加了一些确保 UTF8 编码或 ASCII 编码有效性的约束。

7.2、Documentation

​ 在 Move 的标准库中,我们可以找到定义了字符串类型的 std::stringstd::ascii 模块。它们提供了一系列的方法来创建、操作和判断 String

最常用的创建字符串的方式是通过 utf8() 函数:

use std::string;
let hello = string::utf8(b"Hello");

​ 该函数会检查传入的字节数组是否为合法的 UTF8 编码,如果不合法就会 abort 中止程序。对于一些特殊情况,我们也可以使用 try_utf8() 函数来创建字符串,它会返回一个 Option&lt;String>,如果编码合法就是 Some(String),否则就是 None

1let hello = try_utf8(b"Hello"); //Some(String)
2let invalid = try_utf8(b"\xFF"); //None

​ 在 Move 或其他编程语言中,\xFF 可用于字符串文字中,表示值超出 ASCII 范围的特定字节或字符,通常用于二进制数据或不可打印的字符。

​ 另一种创建字符串的方式是从 ASCII 字符串转换而来,可以调用 from_ascii() 函数:

use std::ascii;
let ascii_string = ascii::string(b"Hello");
let utf8_string = string::from_ascii(ascii_string);

注意:from_ascii() 函数只能接收合法的 ASCII 字符串,这里定义的字节范围是在 0~122 之间,如果传入的不是该范围内的 ASCII 字符串,程序会 abort

Move 语言提供了丰富的 String 操作函数,例如:

  • append: 将两个字符串连接起来。
  • append_utf8: 将字节向量(必须是有效的 UTF-8 编码)追加到字符串末尾。
  • insert: 在指定位置插入一个字符串。
  • sub_string: 截取字符串的子字符串。
  • index_of: 查找字符串中子字符串的索引位置。
  • is_empty: 检查字符串是否为空。
  • length: 获取字符串的长度(以字节为单位)

7.3、FAQ

字符串的底层实现是什么?

​ 无论使用哪种字符串类型,它们的底层实现都是基于字节数组(vector&lt;u8>)。字符串类型只是对字节数组进行了一层包装,提供了一些额外的功能和安全性检查。我们以 std::ascii 字符串为例:

module std::ascii {
    use std::option::{Self, Option};

    /// 该结构存储底层的字节数组
    public struct String has copy, drop, store {
        bytes: vector&lt;u8>,
    }

    /// 把 ASCII 的字节数组转为字符串
    public fun string(bytes: vector&lt;u8>): String {
       let x = try_string(bytes);
       assert!(x.is_some(), EINVALID_ASCII_CHARACTER);
       x.destroy_some()     // 解压并返回其中的值
    }

    /// 校验每一个字节是否为有效的 ASCII
    public fun try_string(bytes: vector&lt;u8>): Option&lt;String> {
        let len = bytes.length();
        let mut i = 0;
        while (i &lt; len) {
            let possible_byte = bytes[i];
            if (!is_valid_char(possible_byte)) return option::none();
            i = i + 1;
        };
        option::some(String { bytes })
    }

    /// 这里的 ASCII 有效范围为 0~122
    public fun is_valid_char(b: u8): bool {
       b &lt;= 0x7F
    }

​ 它使用 String 结构体存储字符串,底层为 u8 类型的字节数组。当我们调用 ascii::string 函数把字节数组转为字符串时,会先调用 try_string 函数依次判断每个字节是否为有效的 ASCII,这里限制了前 122 个可打印的字符串,如果校验通过,则会返回 option::some,并通过 option::destroy_some 函数进行解包,获取相应的字符串。否则返回执行中断,返回 option::none()

7.4、示例代码

展示字符串操作中 to_stringappend、及 insert 函数用法

module hello::string_module {
  use std::string;
  use std::debug;

  #[test]
  fun test_str_valid_utf8() {
      let sparkle_heart = vector[240, 159, 146, 150];
      let s = sparkle_heart.to_string();
      debug::print(&b"--------".to_string());
      debug::print(&s);
      assert!(s.length() == 4, 22);
  }

    #[test]
    fun test_str_append() {
        let mut s = b"abcd".to_string();
        s.append(b"ef".to_string());
        assert!(s == b"abcdef".to_string(), 22)
    }

    #[test]
    fun test_str_insert() {
        let mut s = b"abcd".to_string();
        s.insert(1, b"xy".to_string());
        assert!(s == b"axybcd".to_string(), 22)
    }
}
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
Ch1hiro
Ch1hiro
一名Web3爱好者