Move 的基础类型
Move 有许多内置的原生类型,它们是构成其他类型的基础。原生类型包括:布尔值(Booleans)、无符号整数(Unsigned Integers)和地址(Address)。本节我们介绍布尔类型,其他的会在后面的章节介绍。
Booleans 类型表示一个布尔值,它有两个可能的值:true和 false,布尔类型非常简单,通常用于存储标志并控制程序流程。
在 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;
Move 支持 6 种无符号整数类型(u8, u16, u32, u64, u128, u256),这些类型的值范围从 0 到最大值,具体取决于类型的大小。与 Rust 不同的是,Move 中没有负数和小数。将数值类型细分的目的是减少资源占用和性能优化。
数值的表示可以使用十进制或十六进制形式:
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;
A:对于这些类型的算数操作(加减乘除求余),两个参数(左侧和右侧操作数)必须具有相同的类型。如果需要对不同类型的值进行操作,则需要首先执行强制转换。
当发生溢出或被零除的情况时,所有算术运算都会中止,而不是以上溢、下溢、被零除等数学整数未定义的的方式输出结果,可以有效避免溢出攻击,提高程序等安全性。
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}
在 Move 中,地址 Address 是一种内置的基本类型,用来表示区块链上的位置或账户。一个地址值是一个 256 位( 32 字节)的标识符。通常表示为前缀为 0x 的十六进制字符串,地址不区分大小写。
0xc494732d09de23389dbe99cb2f979965940a633cf50d55caa80ed9e4fc4e521e
上面的地址是有效地址的示例。它的长度为 64 个字符(32 个字节),并以0x开头。
地址类型是 Move 语言中不可或缺的元素,它用于标识以下实体:
●模块包: 每个模块包都有一个唯一的地址,用于区分不同的模块集合。
●账户: Sui 区块链上的每个账户都拥有一个地址,用于存储和管理资产。
●对象: Move 语言中,对象是存储在区块链上的数据结构,每个对象也拥有一个地址。
●交易发送: 交易的发送和接收都需要指定目标地址。
Move 语言中的 Address 可以是数值地址(以 0x 开头),也可以是命名地址(在 Move.toml 文件中进行注册),地址通常以 @ 符号开头,后跟数值或命名标识符。
命名地址的语法遵循 Move 中任何命名标识符的相同规则。数字地址的语法不限于十六进制编码值,任何有效的 u256 数字值都可以用作地址值,例如 42、0xCAFE 和 10_000 都是有效的数字地址文字。
编译时,十六进制数被解释为 32 字节值,对于命名地址,编译器会在 Move.toml 文件中查找标识符并替换为相应的地址。如果在 Move.toml 文件中找不到标识符,编译器将抛出错误。
Sui 中有哪些预留地址?
预留地址是在 Sui 上有特定用途的特殊地址,它们在环境之间保持不变。预留地址通常是易于记忆和输入的简单值。例如,标准库 @std 的地址是 0x1。十六进制的表示方式如下,小于 32 字节的地址在左侧用零填充。
以下是保留地址的一些示例:
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<u8> 类型
let addr_as_u8: vector<u8> = address::to_bytes(@0x1);
// print: 0x0000000000000000000000000000000000000000000000000000000000000001
debug::print(&addr_as_u8);
// 将 vector<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);
}
}
Struct 是最常用的自定义数据类型之一。Move 语言使用 Struct 来表示一组相关数据的集合,它类似于其他编程语言中的结构体或对象。本文将深入介绍 Struct 在 Move 中的用法和特性。
在 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
定义完 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;
MOVE 语言中的解构和销毁操作对于构建健壮、安全的区块链应用程序至关重要。MOVE 是面向对象(资产)的编程,所有创建的资产必须要有一个归属,要么分配给某个所有者,要么用完就必须销毁,这有助于确保数字资产的所有权管理、状态的一致性,以及整体系统的可靠性和性能。
解构 (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。这样就可以独立地访问和操作结构体的各个字段了。
销毁 (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 本身就被安全地销毁了
Vector 是 Move 编程语言中唯一的原生集合类型(位于 std::vector 模块中)。vector<T> 是类型为 T 的同构集合,只能存储相同类型的元素,可以通过从末端推入 push、弹出 pop 操作动态的增加或删除元素。
Vector 在 Move 语言中的应用非常广泛,尤其是在处理数据和构建链上程序时扮演着关键作用。下面我们就来系统地了解一下 Vector 在 Move 语言中的语法、特性和使用方式。
Vector 的类型定义使用 vector 关键字(注意这里是小写),后面加上尖括号<T>中指定元素的类型,其中 T 可以是任意类型。它的类型定义及实例化如下:
// 定义一个空的布尔类型的 Vector
let empty: vector<bool> = vector[];
// 定义一个包含 u8 类型元素的 Vector
let v: vector<u8> = vector[10, 20, 30];
// 定义一个包含 vector<u8> 类型元素的Vector
let vv: vector<vector<u8>> = vector[vector[10, 20], vector[30, 40]];
Move 标准库在 std::vector
模块中提供了各种操作 Vector 的函数:
vector::push_back<T>(v: &mut vector<T>, t: T)
: 在 Vector 末尾添加元素。vector::pop_back<T>(v: &mut vector<T>)
: T: 移除 Vector 的最后一个元素。vector::length<T>(v: &vector<T>): u64
: 返回 Vector 的长度。vector::empty<T>(): vector<T>
: 创建一个空向量vector::is_empty<Element>(v: &vector<T>): bool
: 判断 Vector 是否为空。vector::remove<T>(v: &mut vector<T>, i: u64)
: T: 移除 Vector 指定索引位置的元素。vector::singleton<T>(e: T): vector<T>
: 创建一个包含 T 的大小为 1 的 Vector。vector::contains<T>(v: &vector<T>, e: &T): bool
: 判断 Vector 中是否包含指定元素。vector::borrow<T>(v: &vector<T>, i: u64): &T
: 获取对象向量 v 的第 i 个元素的不可变引用,如果超出范围则终止vector::swap<T>(v : &mut vector<T>, i: u64, j: u64)
:交换向量 v 中第 i 个和第 j 个索引处的元素,如果超出范围则终止vector::index_of<T>(v: &vector<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<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<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);
}
}
Move 中 Vector 的一个常见用例是用 vector<u8>
表示的“字节数组”。这些字节数组非常常见,但它们的可读性非常差,所以 Move 提供了以下两种语法来提升其可读性:字节字符串字面量(Byte Strings Literals)和 十六进制字符串字面量(Hex Strings Literals。
b"Hello!\n"
。这些是ASCII编码的字符串,支持转义序列如\n
、\r
、\t
等,每个字符对应一个字节。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);
}
Option 是 Move 语言中一个非常重要的类型,它用于表示可能存在或不存在的值。Option 的概念借鉴自 Rust 语言 通过使用 Option 可以优雅地处理可能为空的值,避免空指针异常,因为 Option 始终是一个有效的值,即使它不包含任何值,同时也能提高代码可读性,使代码更易于理解,因为它明确表示了某个值可能不存在。 Option 是 Move 标准库中定义的一个通用类型,位于 std::option路径下,它会默认导入到我们的模块中,因此不需要显式引入,其定义如下:
/// 表示可能存在或不存在的值的抽象。
struct Option<Element> has copy, drop, store {
vec: vector<Element>
}
public fun is_none<Element>(t: &Option<Element>): bool {
t.vec.is_empty()
}
从定义可以看出,Option 是一个参数化类型,它包含了一个 vector 类型的字段 vec。当 vec 的长度为 0 时,表示 Option 中不存在值,即 None 状态;当 vec 的长度为 1 时,表示 Option 中存在一个值,即 Some 状态。 Move 标准库为 Option 提供了丰富的操作方法,包括创建 Some 和 None 实例、判断状态、提取值等,可以非常方便地使用它们。
// Return an `Option` containing `e`
public fun some<Element>(e: Element): Option<Element> {
Option { vec: vector::singleton(e) }
}
public fun none<Element>(): Option<Element> {
Option { vec: vector::empty() }
}
创建 Option 实例的示例:
// 创建一个包含值 "Alice" 的 Option
let some_value: Option<vector<u8>> = option::some(b"Alice");
// 创建一个不包含值的 Option
let none_value: Option<vector<u8>> = option::none();
Move 语言本身并不提供内置的字符串类型,但它在标准库中提供了 string
模块来处理字符串数据。该模块定义了表示 UTF-8 编码的字符串类型。此外,标准库还提供了另一个模块 ascii
,它定义了只包含 ASCII 字符的字符串类型。
Move 标准库中的字符串模块,它在底层是一个 vector<u8>
集合类型,每个元素占用一个或多个字节。因此从本质上讲,Move 中的字符串就是一个字节数组,只不过它附加了一些确保 UTF8 编码或 ASCII 编码有效性的约束。
在 Move 的标准库中,我们可以找到定义了字符串类型的 std::string
和 std::ascii
模块。它们提供了一系列的方法来创建、操作和判断 String。
最常用的创建字符串的方式是通过 utf8()
函数:
use std::string;
let hello = string::utf8(b"Hello");
该函数会检查传入的字节数组是否为合法的 UTF8 编码,如果不合法就会 abort 中止程序。对于一些特殊情况,我们也可以使用 try_utf8()
函数来创建字符串,它会返回一个 Option<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 操作函数,例如:
字符串的底层实现是什么?
无论使用哪种字符串类型,它们的底层实现都是基于字节数组(vector<u8>
)。字符串类型只是对字节数组进行了一层包装,提供了一些额外的功能和安全性检查。我们以 std::ascii
字符串为例:
module std::ascii {
use std::option::{Self, Option};
/// 该结构存储底层的字节数组
public struct String has copy, drop, store {
bytes: vector<u8>,
}
/// 把 ASCII 的字节数组转为字符串
public fun string(bytes: vector<u8>): String {
let x = try_string(bytes);
assert!(x.is_some(), EINVALID_ASCII_CHARACTER);
x.destroy_some() // 解压并返回其中的值
}
/// 校验每一个字节是否为有效的 ASCII
public fun try_string(bytes: vector<u8>): Option<String> {
let len = bytes.length();
let mut i = 0;
while (i < 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 <= 0x7F
}
它使用 String 结构体存储字符串,底层为 u8 类型的字节数组。当我们调用 ascii::string
函数把字节数组转为字符串时,会先调用 try_string
函数依次判断每个字节是否为有效的 ASCII,这里限制了前 122 个可打印的字符串,如果校验通过,则会返回 option::some
,并通过 option::destroy_some
函数进行解包,获取相应的字符串。否则返回执行中断,返回 option::none()
。
展示字符串操作中 to_string
、append
、及 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)
}
}
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!