简介
Move是一种安全、沙盒式和形式化验证的开发语言。它诞生于Facebook的Libra项目(后更名为Diem)。 Move让开发者写出灵活的资源管理程序,同时保证安全防止恶意攻击。Move也可用于区块链外的开发场景。 好了,让我们先写一个Hello World程序吧。
Hello World
概要
本节课我们将安装开发环境、配置IDE并在move、aptos、sui下分别写一个简单的Hello World程序
开发环境配置
目前move开发只能在linux或mac下,使用windows的小伙伴可以开启WSL后在linux环境使用,必要情况下需要使用代理加速模块下载速度或使用加速节点替换为国内下载源,参见附录。已经安装过的小伙伴可以自行跳过相关安装指令
# 安装rustup
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# 遇到Proceed with installation (default)时按enter键
# 安装完成后,提示Rust is installed now. Great!
# 会提示让你配置环境变量,跟着他的提示做
# 例如:
# To configure your current shell, run:
# source "$HOME/.cargo/env"
# 检测安装是否成功
cargo --version
# 在用户目录下创建项目集目录
mkdir -p ~/projects && cd ~/projects
# clone项目
git clone https://github.com/move-language/move.git
# 安装编译依赖工具
cd move
./scripts/dev_setup.sh -ypt
# 安装所需依赖
# Proceed with installing necessary dependencies? (y/N) > y
# 更新终端环境
source ~/.profile
# 编译并安装 move cli(需要一段时间)
cargo install --path language/tools/move-cli
# 检测安装是否成功
move --version
# 在用户目录下创建项目集目录
mkdir -p ~/projects && cd ~/projects
# clone项目
git clone https://github.com/aptos-labs/aptos-core.git
# 安装编译依赖工具
cd aptos-core
./scripts/dev_setup.sh
# 更新终端环境
source ~/.cargo/env
# 切换到devnet分支
git checkout --track origin/devnet
# 编译并安装 aptos cli(需要一段时间)
cargo install --path crates/aptos
# 检测安装是否成功
aptos --version
# 在用户目录下创建项目集目录
mkdir -p ~/projects && cd ~/projects
# clone项目
git clone https://github.com/MystenLabs/sui.git
# 安装编译依赖工具
cd sui
# 切换到devnet分支
git checkout --track origin/devnet
# 编译并安装 sui cli(需要一段时间)
cargo install --path crates/sui
# 检测安装是否成功
sui --version
Hello World
hello move
# 创建项目
mkdir -p ~/projects/move_tutorial && cd ~/projects/move_tutorial
move new hello_move
cd hello_move
添加&编辑项目文件
Move.toml
添加MoveNursery依赖
[package]
name = "hello_move"
version = "0.0.0"
[addresses]
std = "0x1"
[dependencies]
MoveStdlib = { git = "https://github.com/move-language/move.git", subdir = "language/move-stdlib", rev = "main" }
MoveNursery = { git = "https://github.com/move-language/move.git", subdir = "language/move-stdlib/nursery", rev = "main" }
sources/my_module.move
在0xCAFE下创建my_module模块,包含speak方法返回字符串
module 0xCAFE::my_module {
use std::string;
use std::debug;
public fun speak(): string::String {
string::utf8(b"Hello World")
}
#[test]
public fun test_speak() {
let res = speak();
debug::print(&res);
let except = string::utf8(b"Hello World");
assert!(res == except, 0);
}
}
scripts/my_script.move
调用my_module::speak方法,打印字符串
script {
use std::debug;
use 0xCAFE::my_module;
fun my_script() {
debug::print(&my_module::speak());
}
}
# 在沙盒环境下发布模块
move sandbox publish
# 运行script
move sandbox run scripts/my_script.move
# 上述命令输出字符串以char code形式展现,我们利用node转换下查看内容
node -e "console.log([72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100].map(code => String.fromCharCode(code)).join(''))"
hello aptos
# 创建项目
mkdir -p ~/projects/move_tutorial && cd ~/projects/move_tutorial
aptos move init --package-dir hello_aptos --name hello_aptos
cd hello_aptos
sources/my_module.move
在0xCAFE下创建my_module模块,包含speak方法返回字符串
module 0xCAFE::my_module {
use std::string;
use std::debug;
public fun speak(): string::String {
string::utf8(b"Hello World")
}
#[test]
public fun test_speak() {
let res = speak();
debug::print(&res);
let except = string::utf8(b"Hello World");
assert!(res == except, 0);
}
}
# 运行测试
aptos move test
# 上述命令输出字符串以char code形式展现,我们利用node转换下查看内容
node -e "console.log([72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100].map(code => String.fromCharCode(code)).join(''))"
hello sui
# 创建项目
mkdir -p ~/projects/move_tutorial && cd ~/projects/move_tutorial
sui move new --install-dir hello_sui hello_sui
cd hello_sui
sources/my_module.move
在hello_sui下创建my_module模块,包含speak方法返回字符串
module hello_sui::my_module {
use std::string;
public fun speak(): string::String {
string::utf8(b"Hello World")
}
#[test]
public fun test_speak() {
assert!(*string::bytes(&speak()) == b"Hello World", 0);
}
}
# 修改Move.toml
# 将rev修改为devnet
# 运行测试
sui move test
总结
本节我们学习了如何搭建开发环境,并在move、aptos、sui下分别写了hello world小程序。我们发现语法上几乎一致,每种环境又有特有的内容。同学们可以随意修改代码编译测试,另外可以查看move
、aptos
、sui
命令行的帮助内容,看看这些命令下还有哪些好玩的东西。接下来我们要专注于Move语法本身,学习Move模块及脚本相关内容,那我们接下来见。
附录
Windows WSL安装Ubuntu
# 安装 Ubuntu-20.04
wsl --install Ubuntu-20.04
如果是初次安装WSL,需要重启电脑。
# 进入wsl环境
wsl -d Ubuntu-20.04
代理配置
安装过程如果出现timeout
的错误说明你的电脑无法正常下载相关资源,包括编译工具、git代码仓库等。
首先我们需要检测你的代理有效
# 终端中输入此命令,网络无法连通情况下,会卡住没有消息返回,此时按ctrl+c结束执行
curl google.com
# 终端中输入此命令,将ip、port替换为你的代理信息,代理运行正常情况下会输出
HTTP_PROXY=[ip]:[port] curl google.com
代理配置包括两部分:
- 配置终端环境代理,可以解决大部分终端运行命令无法正常连接情况
# 终端中输入命令,将ip、port替换为你的代理信息
export HTTP_PROXY=[ip]:[port]
export HTTPS_PROXY=[ip]:[port]
- 配置git代理,可以解决连接github下载源码问题
# 终端中输入命令,将ip、port替换为你的代理信息
git config --global http.proxy [ip]:[port]
git config --global https.proxy [ip]:[port]
github替换为国内下载源
如果配置代理仍然不稳定无法成功运行,可以通过在github.com
前添加gitclone.com/
前缀,来使用国内下载源加速下载过程,这种办法适用于git clone
项目源码,Move.toml
依赖链接。例如
https://github.com/davidiw/aptos-core.git
添加前缀
https://gitclone.com/github.com/davidiw/aptos-core.git
dev_setup.sh
脚本
# 通过 -h 参数可以查看脚本说明,可以发现 -ypt 安装了 cmake clang pkg-config libssl-dev nodejs z3 cvc5 dotnet boogie 等命令,并更新了终端配置
./scripts/dev_setup.sh -h
# -b batch mode, no user interactions and minimal output
# -p update /home/user/.profile
# -t install build tools
# -y installs or updates Move prover tools: z3, cvc5, dotnet, boogie
# -d installs the solidity compiler
# -g installs Git (required by the Move CLI)
# -v verbose mode
# -i installs an individual tool by name
# -n will target the /opt/ dir rather than the /home/user dir. /opt/bin/, /opt/rustup/, and /opt/dotnet/ rather than /home/user/bin/, /home/user/.rustup/, and /home/user/.dotnet/
VSCode Move 插件安装
https://marketplace.visualstudio.com/items?itemName=move.move-analyzer
搜索move-analyzer插件安装
# 进入 move 源码目录
cd ~/projects/move
# 编译安装 move-analyzer 命令行
cargo install --path language/move-analyzer
注意插件和命令行并非同一个,插件会探测系统安装的命令行并利用命令行执行分析
JetBrains 系列 IDE 插件安装
https://plugins.jetbrains.com/plugin/14721-move-language
搜索Move Language插件安装,配置aptos命令行位置。由于本插件由aptos生态的Pontem开发团队开发,主要针对aptos开发,所以部分IDE build功能在其它环境下运行不正常,但不影响move文件语法高亮等基础功能,仍然可以使用。
参考
模块及脚本
概要
本节课我们将介绍一下Move中两个不同的程序Modules和Scripts。
我们首先新建一个 Move 项目:
move new modules_and_scripts
cd modules_and_scripts
创建完新的项目之后,不要忘记修改Move.toml
文件,将MoveNursery依赖加上去。
[package]
name = "modules_and_scripts"
version = "0.0.0"
[addresses]
std = "0x1"
[dependencies]
MoveStdlib = { git = "https://github.com/move-language/move.git", subdir = "language/move-stdlib", rev = "main" }
#将下面MoveNursery依赖添加到Move.toml文件中。
MoveNursery = { git = "https://github.com/move-language/move.git", subdir = "language/move-stdlib/nursery", rev = "main" }
模块(Module)
模块是定义结构类型以及对这些类型进行操作的函数的库。结构类型定义Move的全局存储的模式,模块函数定义更新存储的规则,模块本身也存储在全局存储中。模块相当于智能合约(Smart Contract)。
语法(Syntax)
首先,模块名称可以以字母 a
到 z
或字母 A
到 Z
开头。在第一个字符之后,模块名可以包含下划线 _
、字母 a
到 z
、字母 A
到 Z
或数字 0
到 9
。通常,模块名称以小写字母开头。名为my_module
的模块应该存储在名为my_module.move
的源文件中。
#![allow(unused)] fn main() { module my_module {} module MyTestModule_1 {} }
模块Module的语法结构如下
#![allow(unused)] fn main() { module <address>::<identifier> { (<use> | <friend> | <type> | <function> | <constant>)* } }
其中<address>
是一个有效的命名地址或字面量地址。
字面量地址
字面量是用于表达源码中一个固定值的表示法,整数、浮点数和字符串等等都是字符串。 比如在Java中:
int a = 1;
a
是声明的变量,那赋值符=
后面的1
就是字面量。总之,字面量就是没有用标识符封装起来的量,是“值”的原始状态。那么字面量地址就是一个实际的地址的值,比如
0xCAFE
、0xC0FFEE
都是字面量地址,而命名地址在使用前,要在Move.toml
文件中声明并分配一个字面量地址。
从语法结构中可以看出,Move
语言的模块包含了五种元素,分别是use
、friend
、type
、function
和constant
。从根本上说,模块是types
和functions
的集合, Uses
用来导入其它模块,或者直接导入其它模块中的结构类型和函数。Friends
用来指定同一地址下可信的模块列表。Constants
定义可以在模块中用使用的私有常量。这些元素的详细介绍会在后面的内容中展示。
在sources/
目录下,新建文件并命名为my_module.move
, 然后编写如下代码:
#![allow(unused)] fn main() { module 0xC0FFEE::my_module{ struct Example has drop{ i: u64 } const ENOT_POSITIVE_NUMBER: u64 = 0; public fun is_even(x: u64): bool { let example = Example { i: x }; if(example.i % 2 == 0){ true }else{ false } } } }
module 0xC0FFEE::my_module
这部分指定模块my_module
会被发布到全局存储中0xC0FFEE这个地址之下。
模块也可以用命名地址来声明,在使用命名地址之前,要将该地址的命名和要分配给它的字面量地址添加到Move.toml
文件中。
[package]
name = "modules_and_scripts"
version = "0.0.0"
[addresses]
std = "0x1"
#下面的命名地址添加到Move.toml文件中。
move_dao = "0xC0FFEE"
[dependencies]
MoveStdlib = { git = "https://github.com/move-language/move.git", subdir = "language/move-stdlib", rev = "main" }
#将下面MoveNursery依赖添加到Move.toml文件中。
MoveNursery = { git = "https://github.com/move-language/move.git", subdir = "language/move-stdlib/nursery", rev = "main" }
修改完Move.toml
文件之后,修改在sources/
目录下的my_module.move
文件。
将
#![allow(unused)] fn main() { module 0xC0FFEE::my_module{ }
修改为
#![allow(unused)] fn main() { module move_dao::my_module{ }
即可。
命名地址只存在于源码级别,并且在编译期间,命名地址会被转换成字节码。例如,如果我们有下面的代码:
#![allow(unused)] fn main() { script { fun example() { move_dao::my_module::is_even(7); } } }
我们会将move_dao
编译为0xC0FFEE
,将和下面的代码是等价的:
#![allow(unused)] fn main() { script { fun example() { 0xC0FFEE::my_module::is_even(7); } } }
但是在源码级别,这两个并不等价 - 函数my_module::is_even
必须通过move_dao
命名地址访问,而不是通过分配给该地址的数值访问。
脚本(Script)
脚本(Scripts)是可执行的入口点,类似于传统语言中的主函数main
。脚本通常调用已发布模块的函数来更新全局存储。Scripts是暂时的代码片段,没有发布到全局存储中。
语法(Syntax)
脚本script具有以下结构:
#![allow(unused)] fn main() { script { <use>* <constants>* fun <identifier><[type parameters: constraint]*>([identifier: type]*) <function_body> } }
一个Script块必须在开头声明use
,然后是constants
的内容,最后声明主函数function
。主函数的名称可以是任意的(也就是说,它不一定命名为 main
),是Script block中唯一的函数,可以有任意数量的参数,并且不能有返回值。
在scripts/
目录下,新建文件并命名为my_script.move
, 将下面示例代码编写到文件中:
#![allow(unused)] fn main() { script{ use std::debug; use std::string; use move_dao::my_module::is_even; fun my_script(_x: u64){ assert!(is_even(_x), 0); debug::print(&string::utf8(b"Even")); } } }
代码完成以后,我们把模块发布出去,并执行脚本,查看结果:
move sandbox publish
#--args 9是告诉VM给script中函数传入参数值9
move sandbox run scripts/my_script.move --args 9
运行之后,VM提示了如下信息:
Execution aborted with code 0 in transaction script
这里因为给Script中传入的参数值9
不是偶数,并且调用了断言assert
,所以脚本my_script.move
的运行被中止了, 没有继续往下执行。
如果再次运行脚本my_script.move
,并传入参数值10,应该得到的如下输出:
[debug] (&) { [69, 118, 101, 110] }
# 利用node转换下输出的char code,结果应该Even
node -e "console.log([69, 118, 101, 110] .map(code => String.fromCharCode(code)).join(''))"
脚本Script的功能非常有限—它们不能声明友元、结构类型或访问全局存储, 它们的主要作用主要是调用模块函数.
Move 基础类型
概要
本节课我们会介绍一下 Move 中的基本类型,以及演示一些如何声明和使用的代码。 Move 语言的础类型有整型(Integers)、布尔型(Bool)、地址(Address)。Move没有浮点型(Float)和内建的字符串类型(String),但在 Move 的标准库中的 string
模块借助 u8 的向量实现了 String 类型。
我们这节课还会从 Address 延伸讲一下 Signer 。
我们首先通过新建一个 Move 项目,以便我们边讲边写:
move new primitive_types
cd primitive_types
在 Move.toml
中添加我们需要的依赖,这里主要是我们后面可能需要用到 std::debug::print()
来查看一些结果:
[dependencies]
MoveStdlib = { git = "https://github.com/move-language/move.git", subdir = "language/move-stdlib", rev = "main" }
MoveNursery = { git = "https://github.com/move-language/move.git", subdir = "language/move-stdlib/nursery", rev = "main" }
然后我们新建一个scripts/
目录,在这个目录下新建脚本文件 my_script.move
,编写下面的代码,作为我们这节课程的“白板”:
#![allow(unused)] fn main() { script { use std::debug; fun main() { // Write the code here } } }
当我们想运行一下 main 函数时,可以在命令行中执行如下指令
move sandbox run scripts/my_script.move
Integers
整型变量定义
Move中的整型目前只有3种,分别是u8
、u64
和u128
,都是无符号整型。Move不支持有符号整型(Signed Integers),从目前来看未来也并没有引入有符号整型的计划,但后续应该会引入其他字节长度的无符号整型。
和其他语言的整型一样,占用字节长度决定了可以表示数值大小的范围:
类型 | 范围 |
---|---|
u8 | 0 ~ $2^8-1$ |
u64 | 0 ~ $2^{64}-1$ |
u128 | 0 ~ $2^{128}-1$ |
我们可以有几种不同的方式来声明变量:
1)先定义一个空的变量和类型,再设定它的值
#![allow(unused)] fn main() { let v: u8; v = 10; }
2)在定义变量和类型的同时设定值
#![allow(unused)] fn main() { let v: u8 = 10; }
3)我们也可以不用显式的写明变量的类型,编译器可以通过代码的上下文对变量的类型进行推断,当无法进行推断的时候,编译器默认会认为是u64
类型
#![allow(unused)] fn main() { let v = 10; }
4)也可以将类型添加在字面值的后面
#![allow(unused)] fn main() { let v = 10u64; }
但如果字面值对于变量指定的(或编译器推断的)类型来说太大了,比如下面我们把 256 赋给一个 u8
类型的变量,而 u8
类型的范围是 0-255:
#![allow(unused)] fn main() { let v: u8 = 256; }
编译器就会报错:
error[E04021]: invalid number after type inference
┌─ ./sources/my_script.move:4:21
│
4 │ let _v: u8 = 256;
│ -- ^^^
│ │ │
│ │ Invalid numerical literal
│ │ Annotating the literal might help inference: '256u64'
│ Expected a literal of type 'u8', but the value is too large.
整型变量运算
数学运算
Move 中的整型都可以执行 +
、-
、*
、/
、%
运算,但符号两边变量的类型要求完全一致,也就是说 u8
只能和 u8
进行这些操作,u8
和 u64
就会报错,我们可以尝试一下:
#![allow(unused)] fn main() { let a: u8 = 10; let b: u64 = 10; a + b; }
编译器会提示两个参数不相容
error[E04007]: incompatible types
┌─ ./sources/my_script.move:6:11
│
4 │ let a: u8 = 10;
│ -- Found: 'u8'. It is not compatible with the other type.
5 │ let b: u64 = 10;
│ --- Found: 'u64'. It is not compatible with the other type.
6 │ a + b;
│ ^ Incompatible arguments to '+'
按位运算
整型还支持按位运算 按位与&
、按位或|
、按位亦或^
#![allow(unused)] fn main() { let a:u8 = 10; // 1010 let b:u8 = 9; // 1001 let r_and = a & b; // 1000 -> 8 let r_or = a | b; // 1011 -> 11 let r_xor = a ^ b; // 0011 -> 3 debug::print(&r_and); debug::print(&r_or); debug::print(&r_xor); }
同样,按位运算符两侧变量的类型也要求一致。
位移运算
位移运算有两种,左位移 <<
和右位移 >>
#![allow(unused)] fn main() { let a: u8 = 10; // 1010 let b = a << 1; // 10100 -> 20 let c = a >> 2; // 10 -> 2 debug::print(&b); debug::print(&c); }
这里不要求位移符号的两侧类型相等,但是右侧只能是 u8
类型,这也很容易理解, u8
最大是255,但目前 Move 最多只有 u128
。
需要注意的是位移的位数不能超过或等于类型的字节数,也就是说 u8
、u64
、u128
分别最多只能位移 7、63、127 位:
#![allow(unused)] fn main() { let a: u8 = 10; // 1010 let b = a << 8; // Abort! // Execution failed because of an arithmetic error (i.e., integer overflow/underflow, div/mod by zero, or invalid shift) in script at code offset 2 }
对比运算
Move 中只有整型可以进行对比运算 <
、>
、>=
、<=
,同样,符号两边的变量类型要一致
#![allow(unused)] fn main() { let a: u8 = 10; let b: u8 = 11; let c = a > b; // false debug::print(&c); let c = a < b; // true debug::print(&c); let c = a >= b; // false debug::print(&c); let c = a <= b ; // true debug::print(&c); }
等号与不等号
虽然 Move 中只有整型可以进行对比运算,但是 ==
和 !=
并不是整型独占的。不过不论如何,符号两侧的类型还是要求一致。
#![allow(unused)] fn main() { let a: u8 = 10; let b: u8 = 11; let c = a == b; // false debug::print(&c); let c = a != b; // true debug::print(&c); }
关于相等性其他的一些知识点,会在学习 Move 语言后续的一些特性时讲到。
类型映射
前面的运算基本要求符号两侧的变量类型一致,这样的限制可能会带来一些麻烦。因此 Move 提供了类型映射(casting),可以临时地转换类型,让符号两侧的变量可以执行运算。
只需要通过 (e as T)
的形式就可以实现类型的映射:
#![allow(unused)] fn main() { let a: u8 = 10; let b: u64 = 2; let c = a + (b as u8); debug::print(&c); }
但需要注意的是,只有整型之间可以进行类型映射,并且变量的值不能超出目标类型的范围,例如 256u64
就无法转换成 u8
, 程序会报错退出。
Bool
布尔型的字面值只有 true
和 false
。布尔型可以执行 逻辑与&&
、逻辑或||
、逻辑非!
的运算。
#![allow(unused)] fn main() { let a = true; let b = true; let c = false; let r1 = a || b; // true let r2 = a || b && c; // true let r3 = !a; // false debug::print(&r1); debug::print(&r2); debug::print(&r3); }
在一些语言中,整型在与布尔型运算时会自动进行转换,但在 Move 中是不可以的,尽管整型有类型映射,但类型映射只限制在不同字节长度的整型之间,因此 Move 的整型无法转化为布尔型。 也因此逻辑运算也只能在布尔型之间执行。
Address
地址是 Move 中的一种类型,用于表示全局存储中的位置(或者称为帐户),地址是一个 128bit 数值的标识符。
尽管是一个128位的整型,但 Move 并不允许通过整型来创建地址,也不允许地址进行任何的数学运算,也不允许改变地址,总的来说 Move 不允许地址发生动态的变化。
你可以在运行时通过地址的值来访问对应地址上的资源(Resources,这部分后面的章节会讲到),但不能在运行时通过来访问地址上的模块。 那么怎么来理解这句话呢,首先我们要看一下 Move 链的全局状态是怎么样的,官方提供了一张示意图:
全局状态在 Rust 中的表示大致如下:
#![allow(unused)] fn main() { struct GlobalStorage { resources: Map<address, Map<ResourceType, ResourceValue>> modules: Map<address, Map<ModuleName, ModuleBytecode>> } }
也就是说全局存储了两个 Map ,一个用来存每个地址有哪些资源,一个用来存每个地址有哪些模块。通过运行时只能访问第一个 Map 中的数据,也就是存储资源的那个 Map。
我们再介绍一下关于地址类型的语法,地址有两种类型:数值地址 和 命名地址。 任何有效的 u128 数值都可以用作地址的值。为了和整型区分,地址在使用的时候语法会根据上下文有所差异:
1)被用作表达式时,需要在地址的字面值或者命名标志符前加上 @
符号,这里的表达式也包括作为函数的参数等,例如:
#![allow(unused)] fn main() { let addr_1 = @0xAB; let addr_2 = @1234; let addr_3 = @std; }
2)除此之外可以不用 @
,例如:
#![allow(unused)] fn main() { // import module use 0x9::my_module; // call function std::debug::print(&1); }
命名地址需要我们在 Move.toml 中声明:
[address]
std = "0x1"
addr = "0xC0FFEECAFE"
当编译的时候,编译器会把源码中的命名地址标识符转换成对应的字节码。 所以在编写源码时,一定要注意不要一会儿用命名地址,一会儿用它的数值地址,这会导致代码的可读性变差,并且从源码层面来说,两者并不相同,一个是编译时的参数,一个是常量。
Signer
Signer 是 Move 内建的一种类型,不可以被复制,包含了交易发送者的地址信息。它代表了发送者的权利,也就是说它可以访问发送者地址下的资源。 可以把 Signer 看作是对地址类型的一种结构体封装:
#![allow(unused)] fn main() { struct signer has drop { addr: address } }
我们无法在代码中创建 Signer 类型的变量,只能通过给 Move 虚拟机传参来创建。 Signer
可以通过 address_of
来获取它内部地址的值。
有了 Signer 后,某些函数就可以验证交易发送者是否真的有权限来做这些事情,避免了弄虚作假。
Address & Signer 演示
然后我们简单的演示一下 Address
和 Signer
的使用,我们先在 sources/
下创建 my_module.move
文件:
#![allow(unused)] fn main() { module 0x42::M { struct Coin has key, store{ value: u64 } public fun give_coin(account: &signer) { let coin = Coin { value: 1 }; // Create a 'Coin' move_to(account, coin); // move the coin to account's address as a resource } public fun balance_of(owner: address): u64 acquires Coin { borrow_global<Coin>(owner).value // query the value of the Coin at owner's address } } }
这里我们简单的创建了一个 Coin
结构体,并提供了给某个地址一个 Coin
的方法 give_coin
,以及检查某个地址上 Coin
的值的方法 balance_of
。
这里可能涉及到一些特性和知识点,可以先不管他,只要知道这两个方法是做什么的就行了,后面的课程中会讲到这些点,这里只作简单的演示。
然后我们修改 my_script.move
:
#![allow(unused)] fn main() { script{ use 0x1::debug; use std::signer; use 0x42::M; fun main(account: signer) { M::give_coin(&account); let r = M::balance_of(signer::address_of(&account)); debug::print(&r); } } }
脚本做的事情就是,先给 account 一个 Coin
,然后我们再通过查看地址下 Coin
的 value
来确认是否执行成功了。
代码完成以后,我们把模块发布出去,并执行脚本,查看结果:
move sandbox publish
move sandbox run scripts/my_script.move --signers 0xCD
这里的参数 --signer 0xCD
就是告诉 VM 我们的发送者地址是 0xCD
。
我们可以看到打印的结果,输出 1
,表明操作成功了:
[debug] 1
向量与字符串
向量是 Move 唯一的基本集合类型。vector<T>
是类型为 T
的元素的同构集合,可以通过从末端推入/弹出值来增加和删减向量中的元素。
其中 T
可以是任意的类型,例如:vector<u64>
, vector<address>
,vector<0x42::MyModule::MyResource>
, 以及 vector<vector<u8>>
都是合法的。
任何类型的向量都可以通过字面值创建:
#![allow(unused)] fn main() { let v = vector[1, 2, 3]; }
当然也可以创建一个空向量:
#![allow(unused)] fn main() { let v = vector[]; }
但是当我们单独执行上面这行代码时,编译器会报错:
error[E04010]: cannot infer type
┌─ ./scripts/debug_script.move:8:9
│
8 │ vector[];
│ ^^^^^^^^ Could not infer this type. Try adding an annotation
编译器告诉我们,无非推断向量的类型,尝试添加标注。这是因为向量的类型是从元素的类型或从向量的使用上推断出来的。我们声明了一个空向量,也没有使用它,编译器自然无法判断它的类型。 只要我们后续使用了这个向量,编译器就可以进行推断,例如:
#![allow(unused)] fn main() { let v = vector[]; let another_v = vector[1, 2, 3]; assert!(v != another_v, 0); }
我们知道整型的默认类型是 u64
,所以 another_v
是一个 u64
的向量,当 v
和 another_v
比较时,编译器就会推断出 v
也是一个 u64
的向量。
或者我们可以显式的指明向量的类型:
#![allow(unused)] fn main() { vector<u8>[]; vector<u64>[]; vector<u128>[]; vector<address>[]; vector<bool>[]; vector<M::Coin>[]; let _v: vector<u8> = vector[]; }
vector<u8>
Move 中向量的一个常见用例是表示“字节数组”,用 vector<u8>
表示。这些值通常用于加密目的,例如公钥或哈希结果。
这些值非常常见,以至于提供了特定的语法使其更具可读性,而不是必须使用 vector[]
,其中每个单独的 u8
值都以数字形式指定。
目前支持两种类型的 vector<u8>
字面量,字节字符串(Byte Strings)和十六进制字符串(Hex Sstrings)。
Byte Strings
字节字符串是带引号的字符串字面值,以 b
为前缀,例如:
#![allow(unused)] fn main() { let s = b"Hello, world!"; debug::print(&s); }
可以看到打印的结果是一串整型:
[debug] (&) [72, 101, 108, 108, 111, 44, 32, 119, 111, 114, 108, 100, 33]
字节字符串是允许转义序列的 ASCII 编码字符串。目前,支持的转义序列如下: | 转义序列 | 描述 | | :------: | :---------------------------------------: | | \n | 换行 | | \r | 回车 | | \t | 制表符 | | \\ | 反斜杠 | | \0 | Null | | \" | 引号 | | \xHH | 十六进制进制转义,插入十六进制字节序列 HH |
Hex Sstrings
十六进制字符串是以 x
为前缀的带引号的字符串字面值,例如:
#![allow(unused)] fn main() { let s = x"48656C6C6F210A"; debug::print(&s); }
可以看到打印的结果是:
[debug] (&) [72, 101, 108, 108, 111, 33, 10]
每个字节对,范围从 00 到 FF 都被解析为十六进制编码的 u8
值。
所以每个字节对对应于结果 vector<u8>
中的单个元素。
字节字符串和十六进制字符串本质上都是 vector<u8>
,因此它们的类型实际上是一样的:
#![allow(unused)] fn main() { assert!(b"" == vector<u8>[], 0); // assert!(b"" == vector<u64>[], 0); // Error: Incompatible arguments to '==' assert!(b"" == x"", 1); assert!(b"Hello!\n" == x"48656C6C6F210A", 2); assert!(b"\x48\x65\x6C\x6C\x6F\x21\x0A" == x"48656C6C6F210A", 3); assert!( b"\"Hello\tworld!\"\n \r \\Null=\0" == x"2248656C6C6F09776F726C6421220A200D205C4E756C6C3D00", 4 ); }
标准库 std::vector
Move 标准库中的 std::vector
模块提供了一些用于操作 vector
的函数
empty<Element>(): vector<Element>;
创建一个类型为Element
的空向量
#![allow(unused)] fn main() { let _empty_v = vector::empty<u128>(); }
singleton<Element>(e: Element): vector<Element>
返回一个包含元素e
的长度为1的向量
#![allow(unused)] fn main() { let v = vector::singleton<u64>(123u64); }
length<Element>(v: &vector<Element>): u64
返回向量的长度
#![allow(unused)] fn main() { assert!(vector::length(&v) == 1, 0); }
这时候向量 v
中只有一个元素 123u64
,所以长度是 1
。
push_back<Element>(v: &mut vector<Element>, e: Element)
将元素e
添加到向量v
的末尾
#![allow(unused)] fn main() { vector::push_back(&mut v, 456u64); vector::push_back(&mut v, 789u64); assert!(vector::length(&v) == 3, 0); }
这里需要注意,因为需要对向量 v
的内容进行修改,所以需要传入 v
的可变引用。
pop_back<Element>(v: &mut vector<Element>): Element
从向量v
中移除最后一个元素,并返回
#![allow(unused)] fn main() { assert!(vector::pop_back(&mut v) == 789u64, 0); assert!(vector::length(&v) == 2, 0); // vector::pop_back(&mut _empty_v); // Abort! }
当我们对一个空向量执行这个函数时,程序回异常退出。
borrow<Element>(v: &vector<Element>, i: u64): &Element
获得向量在索引i
处的不可变引用
#![allow(unused)] fn main() { assert!(*vector::borrow(&v, 0) == 123u64, 0); assert!(vector::borrow(&v, 1) == &456u64, 0); // vector::borrow(&v, 2); // Abort! }
函数返回的是 &Element
,使用的时候需要注意类型的匹配。引用一个超出索引范围的元素时,程序会异常退出。
borrow_mut<Element>(v: &mut vector<Element>, i: u64): &mut Element
获得向量在索引i
处的可变引用
#![allow(unused)] fn main() { assert!(v == vector[123, 456], 0); *vector::borrow_mut(&mut v, 0) = 321u64; assert!(v == vector[321, 456], 0); }
我们可以通过这个函数来修改向量内部的元素,注意这里需要用到解引用 *
。
destroy_empty<Element>(v: vector<Element>)
销毁一个空的向量
#![allow(unused)] fn main() { vector::destroy_empty(_empty_v); }
swap<Element>(v: &mut vector<Element>, i: u64, j: u64)
交换向量i
和j
索引处的元素
#![allow(unused)] fn main() { assert!(v == vector[321, 456], 0); vector::swap(&mut v, 0, 1); assert!(v == vector[456, 321], 0); // vector::swap(&mut v, 0, 2); // Abort! }
当 i
或者 j
超出向量索引范围时,程序会异常退出。
reverse<Element>(v: &mut vector<Element>)
反转向量中元素的顺序
#![allow(unused)] fn main() { assert!(v == vector[456, 321], 0); vector::reverse(&mut v); assert!(v == vector[321, 456], 0); }
append<Element>(lhs: &mut vector<Element>, other: vector<Element>)
把other
向量中的元素全部添加到lhs
的末尾
#![allow(unused)] fn main() { assert!(v == vector[321, 456], 0); vector::append(&mut v, vector[1001, 1002, 1003]); assert!(v == vector[321, 456, 1001, 1002, 1003], 0); }
contains<Element>(v: &vector<Element>, e: &Element): bool
判断e
是否在向量中,存在返回true
,否则返回false
#![allow(unused)] fn main() { assert!(vector::contains(&v, &321), 0); }
注意这里的 e
是引用类型 &Element
。
index_of<Element>(v: &vector<Element>, e: &Element): (bool, u64)
如果元素e
位于向量的索引i
处,返回(true, i)
,否则返回false, 0
#![allow(unused)] fn main() { let (if_exist, index) = vector::index_of(&v, &456); assert!(if_exist == true, 0); assert!(index == 1 , 0); let (if_exist, index) = vector::index_of(&v, &999); assert!(if_exist == false, 0); assert!(index == 0 , 0); }
remove<Element>(v: &mut vector<Element>, i: u64): Element
. 移除索引i
处的元素,并返回该元素,后续的元素按照原来顺序往前移
#![allow(unused)] fn main() { assert!(v == vector[321, 456, 1001, 1002, 1003], 0); assert!(vector::remove(&mut v, 2) == 1001, 0); assert!(v == vector[321, 456, 1002, 1003], 0); }
swap_remove<Element>(v: &mut vector<Element>, i: u64): Element
首先将索引i
处的元素与最后的元素交换,然后将最后的元素弹出。上一个函数的复杂度是 O(n),这个函数的复杂度是 O(1),但是不能保持原有的顺序
#![allow(unused)] fn main() { assert!(v == vector[321, 456, 1002, 1003], 0); assert!(vector::swap_remove(&mut v, 1) == 456, 0); assert!(v == vector[321, 1003, 1002], 0); }
销毁和复制 vector
vector<T>
的某些行为取决于元素类型 T
的能力(ability),
例如:如果向量中包含不具有 drop
能力的元素,就不能隐式的丢弃,必须用 vector::destroy_empty
显式销毁。
但前面讲到 vector::destroy_empty
只能销毁空向量,那对于非空的向量,我们应该如何销毁呢?
我们尝试在 module 中编写一个函数来销毁任意向量:
#![allow(unused)] fn main() { fun destroy_any_vector<T>(_vec: vector<T>) { } }
我们想把向量传入函数中,然后隐式的丢弃它,但编译器会报错:
error[E06001]: unused value without 'drop'
┌─ ./sources/debug_module.move:12:47
│
12 │ fun destroy_any_vector<T>(vec: vector<T>) {
│ --- ---------
│ │ │ │
│ │ │ The type 'vector<T>' can have the ability 'drop' but the type argument 'T' does not have the required ability 'drop'
│ │ The type 'vector<T>' does not have the ability 'drop'
│ The parameter 'vec' still contains a value. The value does not have the 'drop' ability and must be consumed before the function returns
│ ╭───────────────────────────────────────────────^
13 │ │ // vector::destroy_empty(vec)
14 │ │ }
│ ╰─────^ Invalid return
编译器告诉我们,T
没有 drop
的能力,因此不能这样丢弃。但如果我们给 T
加上一个限制,要求它具备 drop
的能力:
#![allow(unused)] fn main() { fun destroy_any_vector<T: drop>(_vec: vector<T>) { } }
这样子我们就可以销毁具有 drop
能力的 T
组成向量了。
同样,除非元素类型具有 copy
能力,否则无法复制向量。换句话说,当且仅当 T
具有 copy
能力时,vector<T>
才具有 copy
能力。
#![allow(unused)] fn main() { let x = vector::singleton<u64>(10); let y = copy x; assert!(x == y, 0); // without copy ability // let p = vector::singleton<M::Coin>( M::create_coin(1) ); // let q = copy p; // assert!(p == q, 0); }
u64
因为具有 copy
能力,因此可以复制,但我们在实现 M::Coin
时没有赋予它 copy
的能力,当我们复制时,编译器就会报错。
ps:let y = copy x;
这行代码中,如果不加 copy
关键词也可以编译成功。
这是因为在 rust 中 place expressions 在被求值时,如果该类型实现了 Copy
trait,那么值就会被copy。
如果该类型实现了 Sized
trait,则会被move。【参考资料】
标准库 std::string
前面讲到,字节字符串 和 十六进制字符串 本质上是 vector<u8>
,Move 标准库中也提供了 std::string
模块。并提供了 String
类型:
#![allow(unused)] fn main() { /// A `String` holds a sequence of bytes which is guaranteed to be in utf8 format. struct String has copy, drop, store { bytes: vector<u8>, } }
String
就是对 vector<u8>
的封装,模块内部也提供了一些操作函数:
utf8(bytes: vector<u8>): String
从vector<u8>
构建一个String
,如果字节不能表示一个合法的utf8,则程序终止。
#![allow(unused)] fn main() { let s = string::utf8(vector[72, 101, 108, 108, 111, 33, 10]); debug::print(&s); }
输出结果是:
[debug] (&) { [72, 101, 108, 108, 111, 33, 10] }
try_utf8(bytes: vector<u8>): Option<String>
这个函数和上面类似,但输出是Option
,成功的话Option
内部的向量会包含一个String
, 否则就只有一个空向量:
#![allow(unused)] fn main() { let valid_s = string::try_utf8(vector[72, 101, 108, 108, 111, 33, 10]); let invalid_s = string::try_utf8(vector[72, 101, 108, 108, 111, 33, 255]); debug::print(&valid_s); debug::print(&invalid_s); }
输出结果为:
[debug] (&) { [{ [72, 101, 108, 108, 111, 33, 10] }] }
[debug] (&) { [] }
bytes(s: &String): &vector<u8>
返回对基础字节向量的引用:
#![allow(unused)] fn main() { assert!(*string::bytes(&s) == vector[72, 101, 108, 108, 111, 33, 10], 0); }
is_empty(s: &String): bool
检查字符串是否为空:
#![allow(unused)] fn main() { assert!(string::is_empty(&string::utf8(vector[])) == true, 0); assert!(string::is_empty(&s) == false, 0); }
length(s: &String): u64
返回字符串的长度:
#![allow(unused)] fn main() { let s = string::utf8(vector[72, 101, 108, 108, 111, 33, 10]); assert!(string::length(&s) == 7, 0); }
append(s: &mut String, r: String)
在s
后面追加字符串r
:
#![allow(unused)] fn main() { let s = string::utf8(vector[72, 101, 108]); let r = string::utf8(vector[111, 33, 10]); string::append(&mut s, r); debug::print(&s); }
可以看到输出结果是连个字符串的拼接:
[debug] (&) { [72, 101, 108, 111, 33, 10] }
append_utf8(s: &mut String, bytes: vector<u8>)
.
另一种方法是,直接在字符串后面追加合法的 utf8 字节向量vector<u8>
:
#![allow(unused)] fn main() { let s = string::utf8(vector[72, 101, 108]); let r = vector[111, 33, 10]; string::append_utf8(&mut s, r); debug::print(&s); }
输出结果和前面一致:
[debug] (&) { [72, 101, 108, 111, 33, 10] }
insert(s: &mut String, at: u64, o: String)
在s
中给定的字节索引位置处插入新字符串o
#![allow(unused)] fn main() { let s = string::utf8(vector[72, 101, 108]); let o = string::utf8(vector[111, 33, 10]); string::insert(&mut s, 2, o); debug::print(&s); }
可以看到新的字符串从第二位开始被插入:
[debug] (&) { [72, 101, 111, 33, 10, 108] }
sub_string(s: &String, i: u64, j: u64): String
根据给定的索引i
和j
返回子字符串:
#![allow(unused)] fn main() { let s = string::utf8(vector[72, 101, 108, 108, 111, 33, 10]); let sub_s = string::sub_string(&s, 2, 5); debug::print(&sub_s); }
从结果可以看到,子字符串包括开始的索引 i
但不包括结束的索引 j
:
[debug] (&) { [108, 108, 111] }
index_of(s: &String, r: &String): u64
查询r
字符串在s
中第一次出现的索引值,若果r
不是s
的子字符串,则返回s
的长度:
#![allow(unused)] fn main() { let s = string::utf8(vector[72, 101, 108, 108, 111, 33, 10]); let sub_s = string::utf8(vector[108, 108, 111]); let another_s = string::utf8(vector[108, 108, 112]); assert!(string::index_of(&s, &sub_s) == 2, 0); assert!(string::index_of(&s, &another_s) == string::length(&s), 0); }
引用
概要
本节课我们将介绍一下 Move 中引用References。
Move 支持两种类型的引用:不可变引用&
和可变引用&mut
。不可变引用是只读的,不能修改相关值(或其任何字段)。可变引用通过写入该引用进行修改。Move的类型系统强制执行所有权规则,以避免引用错误。
我们首先新建一个 Move 项目:
move new references
cd references
创建完新的项目之后,不要忘记修改Move.toml
文件,将命名地址move_dao和MoveNursery依赖加上去。
[package]
name = "references"
version = "0.0.0"
[addresses]
std = "0x1"
#将下列命名地址添加到Move.toml文件中
move_dao = "0xC0FFEE"
[dependencies]
MoveStdlib = { git = "https://github.com/move-language/move.git", subdir = "language/move-stdlib", rev = "main" }
#将下面MoveNursery依赖添加到Move.toml文件中。
MoveNursery = { git = "https://github.com/move-language/move.git", subdir = "language/move-stdlib/nursery", rev = "main" }
新建一个scripts/ 目录,在这个目录下新建脚本文件 my_script.move,编写下面的代码:
#![allow(unused)] fn main() { script { use std::debug; fun main() { // Write the code here } } }
引用运算符(Reference Operators)
Move 提供了引用运算符,可以用来创建引用、扩展引用以及把一个可变引用转换成不可变引用。下面的列表是这些运算符的语法,其中,e: T
的表示的是“类型为T
的表达式e
”.
语法 | 类型 | 描述 |
---|---|---|
&e | &T 其中 e: T 和 T 是非引用类型 | 创建一个不可变的引用 e |
&mut e | &mut T 其中 e: T 和 T 是非引用类型 | 创建一个可变的引用 e |
&e.f | &T 其中 e.f: T | 创建结构 e 的字段 f 的不可变引用 |
&mut e.f | &mut T 其中e.f: T | 创建结构 e 的字段 f 的可变引用 |
freeze(e) | &T 其中e: &mut T | 将可变引用 e 转换为不可变引用 |
不可变引用
运算符&e
和&e.f
可以用来创建新的不可变引用, 但是要注意的是,多重引用在Move语言里是不允许的:
#![allow(unused)] fn main() { let token = Token{amount: 1}; let x: u64 = 0; let y: &u64 = &x; //创建u64类型的不可变引用y let t_ref : &Token = &token; //创建Token类型的不可变引用t_ref //下面的语句是错误的,编译时会报错。 let z: &&u64 = &y; }
在结构中,运算符&e
和&e.f
也可以扩展不可变引用:
#![allow(unused)] fn main() { let token = Token{amount: 1}; let t_ref : &Token = &token; let a_ref : &u64 = &t_ref.amount; }
只要两个结构都在同一个模块中,具有多个字段的引用表达式也是可以的:
#![allow(unused)] fn main() { struct Token { amount: B } struct Balance { token : Token } fun f(bal: &Balance): &u64 { &bal.token.amount } }
可变引用
运算符&mut e
和&mut e.f
可以用来创建新的可变引用, 同样也不能多重引用:
#![allow(unused)] fn main() { let token = Token{amount: 1}; let x: u64 = 0; let y: &mut u64 = &mut x; //创建u64类型的可变引用y let t_ref : &mut Token = &mut token; //创建Token类型的可变引用t_ref }
在结构中,运算符&mut e
和&mut e.f
也可以扩展不可变引用, 但是在扩展的时候需要注意的是被扩展的引用必须是可变的,除非扩展的是一个不可变引用:
#![allow(unused)] fn main() { let token = Token{amount: 1}; let t_ref : &mut Token = &mut token; let a_ref : &mut u64 = &mut t_ref.amount; //下列代码是有效的, 但引用a不能更新原token的值, 而引用t可以 let t: &mut Token = &mut token; let a :&u64 = &t.amount; //下列代码无效,编译器会报错 let t: &Token = &token; let a :&mut u64 = &mut t.amount; }
可以调用freeze将一个不可变引用转换成不可变引用.
#![allow(unused)] fn main() { let x: u64 = 0; let y: &mut u64 = &mut x; //创建u64类型的可变引用y let z = freeze(y); //将可变引用y转换成不可变引用并赋给z }
freeze推断(freeze inference)
在Move中,可变引用可以用在期望是不可变引用的位置, 这是因为编译器会在底层需要的地方插入freeze指令。如下:
#![allow(unused)] fn main() { fun takes_immut_returns_immut(x: &u64): &u64 { x } // freeze推断,因为返回值的类型是可变引用,但是期望返回的是不可变类型 fun takes_mut_returns_immut(x: &mut u64): &u64 { x } fun expression_examples() { let x = 0; let y = 0; takes_immut_returns_immut(&x); // 无freeze推断 takes_immut_returns_immut(&mut x); // 有freeze推断,因为期望的参数是一个不可变引用 takes_mut_returns_immut(&mut x); // 无freeze推断 assert!(&x == &mut y, 42); // freeze推断, 因为不等号要求左右两边类型一致 } fun assignment_examples() { let x = 0; let y = 0; let imm_ref: &u64 = &x; imm_ref = &x; // 无freeze推断 imm_ref = &mut y; // freeze推断,因为imm_ref期望被赋值为一个不可变引用 } }
通过freeze推断,Move 类型检查器可以将 &mut T
视为 &T
的子类型。 如上所示,这意味着对于使用 &T
值的任何表达式,也可以使用 &mut T
值。反之则不允许,即不可以在期望使用可变引用的地方,使用不可变引用,这样的程序会报错。
读写操作 (Reading and Writing)
通过引用进行读写的操作的语法如下:
语法 | 类型 | 描述 |
---|---|---|
*e | T 其中 e 为 &T 或 &mut T | 读取 e 所指向的值 |
*e1 = e2 | () 其中 e1: &mut T 和 e2: T | 用 e2 更新 e1 中的值 |
从上述列表中,可以看到读取和写入两种操作都是使用了类C语言中的*
语法, 其中读取中的*e
是一种表达式,但是写入中的*e
是必须发生在行号左边的改动。
读取
在 Move 中,可以读取可变引用和不可变引用来生成引用值的副本
#![allow(unused)] fn main() { let x: u64 = 0; let a: &u64 = &x; let b: &mut u64 = &mut x; let c = *a; let d = *b; }
读取引用会创建值的新副本,为了读取引用,相关类型必须具有copy 能力
, 这样的限制是为了防止复制资源值:
#![allow(unused)] fn main() { struct Token has key {amount: u64} public fun ref_token(t: Token){ let t_ref : &Token = &t; //下面语句会报错,因为Token没有copy能力 let another_token: Token = *t_ref; } }
写入
Move 中只可以写入可变引用。在执行写入表达式 *x = v
会丢弃 x
中的值,并用 v
更新。
#![allow(unused)] fn main() { let x: u64 = 0; let y: &mut u64 = &mut x; *y = 1; assert!(x == 0, 42); }
为了写入引用,相关类型必须具备drop能力
,因为写入引用将丢弃(或“删除”)旧值。此规则可防止破坏资源值:
#![allow(unused)] fn main() { struct Token has key, copy {amount: u64} public fun ref_token(t: Token){ let t_ref : &mut Token = &mut t; //下面语句会报错,因为Token没有drop能力 *t_ref = Token{amount: 100}; } }
所有权(Ownership)
即使同一引用已经有了副本或者扩展, 它依然是可以被复制和扩展的:
#![allow(unused)] fn main() { public fun tokenTest(token: &mut Token){ let t_ref = token; let t_ext = &mut t_ref.amount; let t_ref2 = token; *t_ext = 100; //1 *t_ref = Token{amount: 99};//2 *t_ref2 = Token{amount: 98};//3 } }
对于熟悉Rust所有权系统的程序员来说,这可能会令人惊讶,因为他们并不能接受上面的代码。Move 的类型系统在处理副本方面更加宽松,但在写入前确保可变引用的唯一所有权方面同样严格。可以尝试把上面代码中有注释的三行打乱下顺序看会是什么样的结果。
由于 Move 的持久化全局存储,要求每一个 Move 值都必须是可序列化的,而引用无法被序列化, 所以引用不能存储为结构的字段值的类型,进而了不能存在于全层存储中, 这个规则同样适用于元组。当 Move 程序终止时,程序执行期间创建的所有引用都将被销毁;它们完全是短暂的。这种不变式也适用于没有store能力
的类型的值,不同的是,引用和元组在创建结构类型的时候就不被允许。
示例代码
模块Module
#![allow(unused)] fn main() { module move_dao::my_module{ struct Token has copy, key, drop{ amount: u64 } use std::debug; const EVALUE_NOT_CHANGED :u64 = 1; public fun publish_token(): Token{ Token{amount:0} } public fun manipulate_value(x: u64, y : u64){ let temp :u64 = x; let a: &mut u64 = &mut x; *a = y; assert!(x != temp, EVALUE_NOT_CHANGED); debug::print(&x); } public fun manipulate_token(x: u64, y: u64){ let token = Token{amount: x}; let t: &mut Token = &mut token; *t = Token{amount: y}; assert!(token.amount != x, EVALUE_NOT_CHANGED); debug::print(&token.amount); } public fun manipulate_token_amount(x: u64, y:u64){ let token = Token{amount: x}; let t: &mut Token = &mut token; let t_ext :&mut u64 = &mut t.amount; *t_ext = y; assert!(token.amount != x, EVALUE_NOT_CHANGED); debug::print(&token.amount); } public fun tokenTest(token: &mut Token){ let t_ref = token; let t_ext = &mut t_ref.amount; let t_ref2 = token; *t_ext = 100; *t_ref = Token{amount: 99}; *t_ref2 = Token{amount: 98}; assert!(token.amount == 98, 0); debug::print(token); } } }
脚本Script
#![allow(unused)] fn main() { script { use move_dao::my_module; fun main(a: u64, b: u64) { // Write the code here let token = my_module::publish_token(); my_module::manipulate_value(a, b); my_module::manipulate_token(a, b); my_module::manipulate_token_amount(a, b); my_module::tokenTest(&mut token); } } }
执行脚本,查看结果:
move sandbox publish
#--args 1 2是告诉VM给script中函数传入参数值1和2
move sandbox run scripts/my_script.move --args 1 2
常量
概要
本节课我们将学习 Move 中的常量。 首先我们先新建一个 Move 项目,
move new constants
cd constants
添加依赖项MoveNursery到Move.toml
[package]
name = "modules_and_scripts"
version = "0.0.0"
[addresses]
std = "0x1"
[dependencies]
MoveStdlib = { git = "https://github.com/move-language/move.git", subdir = "language/move-stdlib", rev = "main" }
#将下面MoveNursery依赖添加到Move.toml文件中。
MoveNursery = { git = "https://github.com/move-language/move.git", subdir = "language/move-stdlib/nursery", rev = "main" }
新建一个scripts/ 目录,在这个目录下新建脚本文件 my_script.move,编写下面的代码:
#![allow(unused)] fn main() { script { use std::debug; fun main() { // Write the code here } } }
常量可以让我们定义一个在module
或script
内使用的静态值。
常量声明以 const 关键字开头,后跟名称、类型和值。他们可以存在于脚本或模块中
#![allow(unused)] fn main() { const <name>: <type> = <expression>; }
例子:
假如,有一家商店,一些操作我只想让我自己能够去做,例如拿出收银机里所有的钱。 我可以把我自己的地址设为一个常量,常量的值是无法修改的。 当我们要做操作的时候我们就先去鉴权。
我们先在 sources/ 下创建 my_module.move 文件:
#![allow(unused)] fn main() { address 0x42 { module example { use std::signer; const MY_ADDRESS: address = @0x42; const MY_ERROR_CODE: u64 = 1; public fun permissioned(s: &signer) { assert!(signer::address_of(s) == MY_ADDRESS, MY_ERROR_CODE); } } } }
将MY_ADDRESS和MY_ERROR_CODE设为常量
然后我们修改 script/my_script.move 文件:
#![allow(unused)] fn main() { script{ use 0x42::example; fun main(account: signer) { example::permissioned(&account); } } }
代码完成以后,我们把模块发布出去,并执行脚本
move sandbox publish
move sandbox run scripts/my_script.move --signers 0x43
这里的参数 --signer 0x43 就是告诉 VM 调用permissoned方法的地址是 0x43。 如果参数不为0x42,则会终止,并返回以下信息
Execution aborted with code 1 in module 00000000000000000000000000000042::example.
需要注意的是,常量必须以大写字母A
到Z
开头,后面可以用可以包含下划线 _
、字母 a
到 z
、字母 A
到 Z
或数字 0
到 9
,否则将会报错
虽然包含小写字母的写法是被允许的,但是编码规范中常量的定义只使用大写字母,
每个单词之间用下划线分割。
#![allow(unused)] fn main() { const FLAG: bool = false; const MY_ERROR_CODE: u64 = 0; const ADDRESS_42: address = @0x42; }
这种以 A 到 Z 开头的命名限制是为了给未来的语言特性留出空间。此限制未来可能会保留,也可能会删除。
可见性 (Visibility)
目前不支持 public 常量。 const 值只能在声明的模块中使用。
有效值
目前,常量仅限于原始类型 bool、u8、u64、u128、address 和vector
#![allow(unused)] fn main() { const MY_BOOL: bool = false; const MY_ADDRESS: address = @0x70DD; const BYTES: vector<u8> = b"hello world"; const HEX_BYTES: vector<u8> = x"DEADBEEF"; }
表达式作为值
除了字面量,常量也可以包含更复杂的表达式,只要编译器能够在编译时将表达式reduce为一个值。 目前,相等运算、布尔运算、按位运算和算术运算都是可以使用。
#![allow(unused)] fn main() { const RULE: bool = true && false; const CAP: u64 = 10 * 100 + 1; const SHIFTY: u8 = { (1 << 1) * (1 << 2) * (1 << 3) * (1 << 4) }; const HALF_MAX: u128 = 340282366920938463463374607431768211455 / 2; const EQUAL: bool = 1 == 1; }
如果操作会导致运行时异常,编译器会给出无法生成常量值的错误。
还有一点需要补充的是,常量当前不能引用其他常量。将来会支持。