模块及脚本

概要

本节课我们将介绍一下Move中两个不同的程序ModulesScripts

我们首先新建一个 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)

首先,模块名称可以以字母 az 或字母 AZ 开头。在第一个字符之后,模块名可以包含下划线 _ 、字母 az 、字母 AZ 或数字 09。通常,模块名称以小写字母开头。名为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就是字面量。总之,字面量就是没有用标识符封装起来的量,是“值”的原始状态。
那么字面量地址就是一个实际的地址的值,比如0xCAFE0xC0FFEE都是字面量地址,而命名地址在使用前,要在Move.toml文件中声明并分配一个字面量地址。

从语法结构中可以看出,Move语言的模块包含了五种元素,分别是usefriendtypefunctionconstant。从根本上说,模块是typesfunctions的集合, 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的功能非常有限—它们不能声明友元、结构类型或访问全局存储, 它们的主要作用主要是调用模块函数.