3.轻松入门Move: 清单文件和模块

按照国际惯例,学习一门语言,编写的第一个程序一定是输出一个Hello World。今天我们也来一起写一个Hello World并以此引出一些Move项目结构,并作详细介绍

首先我们新建一个名为hello_world的项目,使用命令:

sui move new hello_world

这个命令会自动新建一个名为hello_world的文件夹,文件夹结构如图:

这个文件夹包含一个sources文件夹和一个Move.toml清单文件,其中sources目录是存放我们编写的代码,里面的一个文件对应一个模块。Move.toml文件则是一个清单文件,用于申明包的元数据信息、依赖、地址等。详情请看下面代码块:

[package]
#在这个部分申明包的元数据信息,比如名称、版本信息、证书信息等
name = "hello_world"

# edition = "2024.alpha" # 使用Move 2024 alpha 版本 
# license = ""           # 申明证书,比如, "MIT", "GPL", "Apache 2.0"
# authors = ["..."]      # 申明作者,比如 ["Joe Smith (joesmith@noemail.com)", "John Snow (johnsnow@noemail.com)"]

[dependencies]
#在这个部分列出这个包依赖的其他包
Sui = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/sui-framework", rev = "framework/testnet" }

# 对于远程包的引用,请使用 `{ git = "...", subdir = "...", rev = "..." }`.
# 其中rev可以是一个分支,一个tag或者一个提交哈希,如下例:
# MyRemotePackage = { git = "https://some.remote/host.git", subdir = "remote/path", rev = "main" }

# 如果是本地包的引用,使用 `local = path`即可. Path 是包的根目录下的相对路径
# Local = { local = "../path/to" }

# 如果有版本冲突,则指定版本号,并且使用`override = true`来解决冲突
# Override = { local = "../conflicting/version", override = true }

[addresses]
# 申明这个包的地址,在后续可以使用hello_world代指这个包。默认情况这个地址是"0x0",但在发布到链上会替换成区块链上的地址。这个名称甚至不局限于在包内使用,比如std标准包,我们直接在自己的包中使用std引用。
hello_world = "0x0"

[dev-dependencies]
# 这个部分用于声明开发或者测试模式下才需要的依赖。
# 额外提一句,开发或测试模式是编译时通过指定--test(--dev)来指定模式的。
# Local = { local = "../path/to/dev-build" }

[dev-addresses]
# 这个部分用于申明开发或者测试模式下的包地址。

上面代码块中提到的package就是所谓的包,包是一组模块的集合,是发布代码到链上的单元。

那什么又是模块呢?模块是一组函数和结构体的集合。模块是一种组织代码的方式,可以将相关的功能组织在一起,并且通过模块可以控制代码的可见性和访问权限,提高代码的可维护性和可扩展性。

现在我们在sources文件夹内新建一个文件,命名为helloworld.move,然后编写一个名为hello_world的模块:

#![allow(unused)]
fn main() {
module hello_world::hello_world {
    use sui::object::{Self, UID};
    use sui::transfer;
    use sui::tx_context::{Self, TxContext};

    public struct HelloWorldObject has key, store {
        id: UID,
        text: std::string::String 
    }

    #[lint_allow(self_transfer)]
    public fun mint(ctx: &mut TxContext) {
        let object = HelloWorldObject {
            id: object::new(ctx), 
            text: std::string::utf8(b"Hello World!") 
        };
        transfer::public_transfer(object, tx_context::sender(ctx));
    }
}
}

上面这段代码申明了一个名称为HelloWorldObject的结构体,在mint方法中创建一个HelloWorldObject对象并将所有权转交给当前上下文的用户。此段代码包含以下几个知识点:

1.模块的申明方法

module <包的地址>::<模块名称> {
	模块内容
}

包的地址和模块名称可以标识一个模块,包的地址可以是包名也可以是清单文件中申明的包地址。

在一个包内,模块名必须唯一。模块文件名与模块名不一致可以通过编译也不影响其他模块的调用,但不建议这么做。模块名建议使用小写字母和下划线组成。

2.模块之间的关系:引用

模块之间可以互相引用,引用方式分为以下几种:

  • 直接引用:

    #![allow(unused)]
    fn main() {
    public struct HelloWorldObject has key, store {
    	id: UID,
        text: std::string::String //直接引用std::string模块的utf8方法
    }
    }
  • 使用use引用结构体或者函数

    #![allow(unused)]
    fn main() {
    use sui::object::UID //申明引用UID结构体(或函数)
    public struct HelloWorldObject has key, store {
    	id: UID, //直接使用结构体名(或函数)
        text: std::string::String 
    }
    }
  • 使用use 引用模块

    #![allow(unused)]
    fn main() {
    use sui::transfer;
    transfer::public_transfer(object, tx_context::sender(ctx));
    }
  • 使用Self关键字引用模块自身

    #![allow(unused)]
    fn main() {
     use sui::tx_context::{Self, TxContext};
     tx_context::sender(ctx)//使用模块名调用函数
     //直接使用TxContext引用TxContext结构体
    }
  • 同一个模块多个引用

    #![allow(unused)]
    fn main() {
    使用花括号括起来,并逗号隔开
    use sui::tx_context::{Self, TxContext};
    }

不同的申明方式之间也可以转换使用,效果是一样。比如:

#![allow(unused)]
fn main() {
use sui::tx_context::{Self, TxContext};
tx_context::sender(ctx)//使用模块名调用函数
//直接使用TxContext引用TxContext结构体
}

等价于:

#![allow(unused)]
fn main() {
use sui::tx_context;
tx_context::sender(ctx)//使用模块名调用函数
//TxContext结构体则使用tx_context::TxContext引用
}

还等价于:

#![allow(unused)]
fn main() {
use sui::tx_context::{sender, TxContext};
sender(ctx)//直接调用函数
//直接使用TxContext引用TxContext结构体
}

3.模块如何控制访问权限?

我们上面讲了如何引用模块,那如果模块不愿意被引用怎么办呢?这就涉及到访问权限的问题。访问模块内容分为访问结构体和函数。而结构体内部的字段不能跨模块使用,只能通过调用与结构体同模块的函数实现,如下图:

#![allow(unused)]
fn main() {
 struct HelloWorldObject has key, store {
        id: UID,
        text: std::string::String 
  }
  //通过调用此方法访问结构体内部值
  public fun getText(obj: &HelloWorldObject) :std::string::String {
        obj.text
  }
}

所以,访问权限其实主要就是对函数的访问权限的控制。对函数的访问权限控制粒度从粗到细分为:

  • 所有模块都可以调用

    所有模块都可调用,使用关键字public申明函数即可

  • 部分模块可以调用

    使用关键词public(package)申明函数,那就只有在模块内申明了是“朋友”的模块才可以调用。如下:

    #![allow(unused)]
    fn main() {
    //申明朋友模块
    //只有朋友模块才可以引用的函数
    public(package) fun getName(obj: &Game) :std::string::String {
        obj.name
    }
    }
  • 只有同模块可调用

​ 只有同模块都可调用,使用关键字private申明函数即可。private是默认权限,也可以省略。

了解更多Move内容:

  • telegram: t.me/move_cn
  • QQ群: 79489587