前言
一些疑问:
- sui 和move是什么关系?
基础
基本数据类型
Move 的基本数据类型包括: 整型 (u8, u32,u64, u128,u258)、布尔型 boolean 和地址 address。
Move 不支持字符串和浮点数。
_u8:代表8位无符号整数类型,范围是0~255。占用内存8位
_u16:代表16位无符号整数类型,范围是0~65535。占用内存16位
_u32:代表32位无符号整数类型,范围是0~4294967295。占用内存32位
_u64:代表64位无符号整数类型,范围是0~18446744073709551615。占用内64位
以此类推
整型
module ch02::int {
fun main() {
// define empty variable, set value later
let a: u8;
a = 10;
let a = 1u32;
// define variable, set type
let a: u64 = 10;
// finally simple assignment
let a = 10;
// simple assignment with defined value type
let a = 10u64;
// in function calls or expressions you can use ints as constant values
if (a < 10) {};
// or like this, with type
if (a < 10u64) {}; // usually you don't need to specify type
let b = 1u256;
// or like this, with type
if (b < 10u256) {}; // usually you don't need to specify type
}
}
let a = 10 默认不手动标记类型的整型是 u64 类型,也就是等同于 let a:u64 = 10 或者 let a = 10u64
布尔型
布尔类型就像编程语言那样,包含false和true两个值。
module book::boolean {
fun main() {
// these are all the ways to do it
let b : bool; b = true;
let b : bool = true;
let b = true;
let b = false; // here's an example with false
}
}
地址
地址是区块链中交易发送者的标识符,转账和导入模块这些基本操作都离不开地址。
module book::addr {
fun main() {
let addr: address; // type identifier
addr = @ch02;
}
}
模块
模块是发布在特定地址下的打包在一起的一组函数和结构体。
模块以module关键字开头,后面跟随地址::模块名称和大括号,大括号中放置模块内容。
module book::math {
public fun sum(a: u64, b: u64): u64 {
a + b
}
}
注意:
- 模块在发布者的地址下发布。标准库在 0x1 地址下发布。
- 发布模块时,不会执行任何函数。要使用模块就得使用脚本。
- 模块名推荐使用小写
- 模块是发布代码供他人访问的唯一方法。新的类型和 Resource 也只能在模块中定义。默认情况下,模块将在发布者的地址下进行编译和发布
导入
Move 在默认上下文中只能使用基本类型,也就是整型、布尔型和地址,可以执行的有意义或有用的操作也就是操作这些基本类型,或者基于基本类型定义新的类型。
除此之外还可以导入已发布的模块(或标准库)。
- 直接导入
module book::m {
fun main(a: u8) {
std::debug::print(&a);
}
}
在此示例中,我们从地址0x1(标准库)导入了 debug 模块,并使用了它的 print 方法
use关键字
要使代码更简洁(注意,0x1 是特殊的地址,实际地址是很长的),可以使用关键字use:
use <address>::<ModuleName>;
这里 </address/> 是模块发布object的地址, 是模块的名字。非常简单,例如,我们可以像下面这样从 0x1 地址导入 vector 模块。
use 0x1::vector;
- 访问模块的内容
要访问导入的模块的方法(或类型),需要使用::符号。非常简单,模块中定义的所有公开成员都可以通过双冒号进行访问。
module book::m_use {
use std::debug::print;
fun main(a: u8) {
print(&a);
}
}
在模块中导入
在模块中导入模块必须在 module {} 块内进行:
module book::math {
use std::vector;
// you are free to import any number of modules
public fun empty_vec(): vector<u64> {
let v = vector::empty<u64>();
v
}
}
成员导入
导入语句还可以进一步被扩展,可以直接导入模块的成员:
module book::m_use2 {
// single member import
use sui::tx_context::TxContext;
use sui::tx_context::sender;
// multi member import (mind braces)
use std::vector::{
empty,
push_back
};
fun main(ctx: &mut TxContext) {
// use functions without module access
let vec = empty<u8>();
push_back(&mut vec, 10);
let _ = sender(ctx);
}
}
使用 Self 来同时导入模块和模块成员
导入语句还可以进一步扩展,通过使用 Self 来同时导入模块和模块成员,这里 Self 代表模块自己。
module book::m_self {
use 0x1::vector::{
Self, // Self == Imported module
empty
};
fun main() {
// `empty` imported as `empty`
let vec = empty<u8>();
// Self means vector
vector::push_back(&mut vec, 10);
}
}
使用 use as
当两个或多个模块具有相同的名称时,可以使用关键字as更改导入的模块的名称,这样可以在解决命名冲突的同时缩短代码长度。
语法:
use <address>::<ModuleName> as <Alias>;
module ch04::m_as1 {
use 0x1::vector::{
Self as v,
empty as empty_vec
};
fun main() {
// `empty` imported as `empty_vec`
let vec = empty_vec<u8>();
// Self as V = vector
v::push_back(&mut vec, 10);
}
}
函数
Move 中代码的执行是通过调用函数实现的。函数以 fun 关键字开头,后跟函数名称、扩在括号中的参数,以及扩在花括号中的函数体。
module book::f01 {
fun function_name(arg1: u64, arg2: bool): u64 {
// function body
10
}
}
- 注意:Move 函数使用snake_case命名规则,也就是小写字母以及下划线作为单词分隔符。
返回值:
module book::math {
fun zero(): u8 {
0
}
}
第一步:我们定义一个 math 模块,它有一个函数:zero(),该函数返回 u8 类型的值 0。0 之后没有分号,因为它是函数的返回值
return关键字:
module book::m {
public fun conditional_return(a: u8): bool {
if (a == 10) {
return true // semi is not put!
};
if (a < 10) {
true
} else {
false
}
}
}
多个返回值及解构:
要指定多个返回值,需要使用括号:
module book::math {
// ...
public fun max(a: u8, b: u8): (u8, bool) {
if (a > b) {
(a, false)
} else if (a < b) {
(b, false)
} else {
(a, true)
}
}
}
在另一个模块中使用该函数的返回值。
module book::math_use {
use book::math::sum;
use book::math::max;
fun use_max(){
let (a,b)= max(1u8,2u8);
}
}
上面例子中,我们解构了一个二元组,用函数 max 的返回值创建了两个新变量。
公有、私有方法、friend方法、native本地方法:
默认情况下,模块中定义的每个函数都是私有的,无法在其它模块或脚本中访问。可能你已经注意到了,我们在 Math 模块中定义的某些函数前有关键字 public:
- 关键字 public 将更改函数的默认可见性并使其公开,即可以从外部访问。
- 默认情况下函数是私有函数只能在定义它们的模块中访问。
- 私有函数只能在定义它们的模块中访问。
module book::math {
public fun sum(a: u64, b: u64): u64 {
a + b
}
fun zero(): u8 {
0
}
}
friend 方法:
friend 方法可以指定指定的模板能调用,目前只能在同一个包内生效
module book::friends {
friend book::m;
public(friend) fun a_less_10(a: u8): bool {
if(a < 10u) return true;
false
}
}
本地方法:
有一种特殊的函数叫做"本地方法"。本地方法实现的功能超出了 Move 的能力,它可以提供了额外的功能。本地方法由 VM 本身定义,并且在不同的VM实现中可能会有所不同。这意味着它们没有用 Move 语法实现,没有函数体,直接以分号结尾。关键字 native 用于标记本地函数,它和函数可见性修饰符不冲突,native 和 public 可以同时使用。
module book::m {
native public fun borrow_address(s: &signer): &address;
// ... some other functions ...
}
运算符
as
as 在move 中有两个用法:
1.给包取别名
module book::m_as1 {
use 0x1::vector::{
Self as v,
empty as empty_vec
};
fun main() {
// `empty` imported as `empty_vec`
let vec = empty_vec<u8>();
// Self as V = vector
v::push_back(&mut vec, 10);
}
}
2.整型类型转换 语法 (整型A as 整型 B) 当需要比较值的大小或者当函数需要输入不同大小的整型参数时,你可以使用as运算符将一种整型转换成另外一种整型 注意就是括号是一定不能省的
module book::op_as {
fun main(){
let _a:u64 = (10u8 as u64);
let _b:u8 = (a as u8);
}
}
+ - * /
注意:
- 负数做减法一定要检查是否产生负数
- 做加法乘法注意 溢出报错
- 除法小心精度丢失问题
- 得益于Move的安全设计,溢出和负数不会让合约产生安全问题,因为程序会终止运行,但是程序终止会给用户带来不好的体验,代码不是很好调试,建议还是做好溢出边界判断
module book::op_arith {
fun main(){
let _add_op = 1 + 1;
let _mut_op = 1*1;
let _minu_op = 100 -1;
let _div_op = 100/1;
}
}
常量
Move 支持模块级常量。常量一旦定义,就无法更改,所以可以使用常量为特定模块或脚本定义一些不变量,例如角色、标识符等。
常量可以定义为基本类型(比如整数,布尔值和地址),也可以定义为数组。我们可以通过名称访问常量,但是要注意,常量对于定义它们的模块来说是本地可见的。
module book::consts {
use std::debug;
const RECEIVER: address = 0x999;
const ErrO1: u64 = 1000
fun main(account: &signer) {
debug::print<address>(&RECEIVER);
let _ = RECEIVER;
let _ = ErrO1;
}
}
注意:
- 一旦定义,常量是不可更改的。
- 常量在模块是本地可见的,不能在外部使用。
- 可以将常量定义为一个表达式(带有花括号),但是此表达式的语法非常有限。
表达式和作用域
在编程语言中,表达式是具有返回值的代码单元。有返回值的函数调用是一个表达式,它有返回值;整型常数也是一个表达式,它返回整数;其它表达式依此类
- 表达式必须用分号";"隔开
空表达式
类似于 Rust,Move 中的空表达式用空括号表示
module book::exp_scope {
fun empty() {
() // this is an empty expression
}
}-
文字(Literal)表达式
下面的代码,每行包含一个以分号结尾的表达式。最后一行包含三个表达式,由分号隔开。
module book::exp_scope {
fun main() {
10;
10 + 5;
true;
true != false;
0x1;
1; 2; 3;
}
变量和let关键字
关键字 let 用来将表达式的值存储在变量中,以便于将其传递到其它地方。我们曾经在基本类型章节中使用过 let,它用来创建一个新变量,该变量要么为空(未定义),要么为某表达式的值。
module book::exp_scope {
fun main() {
let a;
let b = true;
let c = 10;
let d = 0x1;
a = c;
}
}
-
关键字 let 会在当前作用域内创建新变量,并可以选择初始化此变量。该表达式的语法是:let : ;或let = 。
-
创建和初始化变量后,就可以使用变量名来修改或访问它所代表的值了。在上面的示例中,变量 a 在函数末尾被初始化,并被分配了一个值 c。
-
等号"="是赋值运算符。它将右侧表达式赋值给左侧变量。示例:a = 10 表示将整数10赋值给变量a。
整型运算符
Move具有多种用于修改整数值的运算符:
下划线 “_” 表示未被使用
Move 中每个变量都必须被使用,否则代码编译不会通过, 因此我们不能初始化一个变量却不去使用它。但是你可以用下划线来告诉编译器,这个变量是故意不被使用的。
例如,下面的脚本在编译时会报错:
module book::exp_scope {
fun main() {
let a = 1;
}
}
报错:
┌── /scripts/script.move:3:13 ───
│
33 │ let a = 1;
│ ^ Unused assignment or binding for local 'a'. Consider removing or replacing it with '_'
│
编译器给出明确提示:用下划线来代替变量名。
module book::exp_scope {
fun main() {
let _ = 1;
}
}
屏蔽
Move 允许两次定义同一个的变量,第一个变量将会被屏蔽。但有一个要求:我们仍然需要"使用"被屏蔽的变量。
- 被屏蔽的变量仍要使用,
module book::exp_scope {
fun main() {
let a = 1//这个a实际未使用 let a = 2;
let _ = a;
}
}
在上面的示例中,我们仅使用了第二个a。第一个a实际上未使用,因为a在下一行被重新定义了。所以,我们可以通过下面的修改使得这段代码正常运行
module book::exp_scope {
fun main() {
let a = 1;
let a = a + 2;
let _ = a;
}
}
块表达式
块表达式用花括号"{}"表示。块可以包含其它表达式(和其它代码块)。函数体在某种意义上也是一个代码块。
module book::exp_scope {
fun block() {
{ };
{ { }; };
true;
{
true;
{ 10; };
};
{ { { 10; }; }; };
}
}
作用域
作用域是绑定生效的代码区域。换句话说,变量存在于作用域中。Move 作用域是由花括号扩起来的代码块,它本质上是一个块。
- 定义一个代码块,实际上是定义一个作用域。
module book::exp_scope {
fun scope_sample() {
// this is a function scope
{
// this is a block scope inside function scope
{
// and this is a scope inside scope
// inside functions scope... etc
};
};
{
// this is another block inside function scope
};
}
}
从该示例可以看出,作用域是由代码块(或函数)定义的。它们可以嵌套,并且可以定义多个作用域,数量没有限制。
变量的生命周期和可见性
我们前面已经介绍过关键字 let 的作用,它可以用来定义变量。有一点需要强调的是,该变量仅存在于变量所处的作用域内。也就是说,它在作用域之外不可访问,并在作用域结束后立即消亡。
module book::exp_scope {
fun let_scope_sample() {
let a = 1; // we've defined variable A inside function scope
{
let b = 2; // variable B is inside block scope
{
// variables A and B are accessible inside
// nested scopes
let c = a + b;
}; // in here C dies
// we can't write this line
// let d = c + b;
// as variable C died with its scope
// but we can define another C
let c = b - 1;
}; // variable C dies, so does C
// this is impossible
// let d = b + c;
// we can define any variables we want
// no name reservation happened
let b = a + 1;
let c = b + 1;
} // function scope ended - a, b and c are dropped and no longer accessible
}
- 变量仅存在于其作用域(或代码块)内,当作用域结束时变量随之消亡。
块返回值
上面我们了解到代码块是一个表达式,但是我们没有介绍为什么它是一个表达式以及代码块的返回值是什么。
- 代码块可以返回一个值,如果它后面没有分号,则返回值为代码块内最后一个表达式的值。
module book::exp_scope {
fun block_ret_sample() {
// since block is an expression, we can
// assign it's value to variable with let
let a = {
let c = 10;
c * 1000 // no semicolon! 无分号,返回值为当前表达式
}; // scope ended, variable a got value 10000
let b = {
a * 1000 // no semi!
};
// variable b got value 10000000
{
10; // see semi!有分号,无返回值
}; // this block does not return a value
let _ = a + b; // both a and b get their values from blocks
}
}
- 代码块中的最后一个表达式(不带分号)是该块的返回值。
注意:
- 每个表达式都必须以分号结尾,除非它是 block 的返回值。
- 关键字 let 使用值或表达式创建新变量,该变量的生命周期与其作用域相同。
- 代码块是一个可能具有也可能没有返回值的表达式。
控制流
通过控制流表达式,我们可以选择运行某个代码块,或者跳过某段代码而运行另一个代码块。
Move 支持 if 表达式和循环表达式。
if 表达式
if 表达式允许我们在条件为真时运行代码块,在条件为假时运行另一个代码块。
module book::c_if {
use std::debug;
fun main() {
let a = true;
if (a) {
debug::print<u8>(&0);
} else {
debug::print<u8>(&99);
};
}
}
这个例子中,当a == true时打印0,当a是false时打印99,语法非常简单:
if (<布尔表达式>) <表达式> else <表达式>;
if是一个表达式,我们可以在 let 声明中使用它。但是像所有其他表达式一样,它必须以分号结尾。
module book::c_if {
use std::debug;
fun main() {
// try switching to false
let a = true;
let b = if (a) { // 1st branch
10
} else { // 2nd branch
20
};
debug::print<u8>(&b);
}
}
现在,b 将根据 a 表达式为变量分配不同的值。但是 if 的两个分支必须返回相同的类型!否则,变量 b 将会具有不同类型,这在静态类型的语言中是不允许的。在编译器术语中,这称为分支兼容性 —— 两个分支必须返回兼容(相同)类型。
if 不一定非要和 else 一起使用,也可以单独使用。
module book::c_if {
use std::debug;
fun main() {
let a = true;
// only one optional branch
// if a = false, debug won't be called
if (a) {
debug::print<u8>(&10);
};
}
}
- 但是请记住,**不能在 let 赋值语句中使用不带分支的表达式!**因为如果 if 不满足条件,就会导致变量未被定义,这同样是不允许的。
循环表达式
在 Move 中定义循环有两种方法:
while 条件循环
loop 无限循环
while 条件循环
while 是定义循环的一种方法:在条件为真时执行表达式。只要条件为 true,代码将一遍又一遍的执行。条件通常使用外部变量或计数器实现。
module book::c_if {
fun main() {
let i = 0; // define counter
// iterate while i < 5
// on every iteration increase i
// when i is 5, condition fails and loop exits
while (i < 5) {
i = i + 1;
};
}
}
需要指出的是,while 表达式就像 if 表达式一样,也需要使用分号结束。while 循环的通用语法是:
while (<布尔表达式>) <表达式>;
- 与 if 表达式不同的是,while 表达式没有返回值,因而也就不能像 if 那样把自己赋值给某变量。
无法访问的代码
安全是 Move 最显著的特性。**出于安全考虑,Move 规定所有变量必须被使用。**并且出于同样的原因,Move 禁止使用无法访问的代码。由于数字资产是可编程的,因此可以在代码中使用它们(我们将在 Resource 一章中对其进行介绍)。而将资产放置在无法访问的代码中可能会带来问题,并造成损失。
这就是为什么无法访问的代码如此重要的原因。
无限循环
Move 提供了一种定义无限循环的方法,它没有条件判断,会一直执行。一旦执行该代码将消耗所有给定资源(交易费),大多数情况下,编译器也无法判断循环是否是无限的,也就无法阻止无限循环代码的发布。因此,使用无限循环时一定要注意安全,通常情况下建议使用 while 条件循环。
无限循环用关键字 loop 定义。
module book::c_if {
fun main() {
let i = 0;
loop {
i = i + 1;
};
// UNREACHABLE CODE
let _ = i;
}
}
下面的代码也是可以编译通过的:
module book::c_if {
fun main() {
let i = 0;
loop {
if (i == 1) { // i never changed
break // this statement breaks loop
}
};
// actually unreachable
std::debug::print<u8>(&i);
}
}
对于编译器而言,要了解循环是否真的是无限的,这是一项艰巨的任务,因此,就目前而言,只有开发者自己可以帮助自己发现循环错误,避免资产损失。
通过 continue 和 break 控制循环
continue 和 break 关键字,分别允许程序跳过一轮循环或中断循环,可以在两种类型的循环中同时使用它们。
例如,让我们在 loop 中添加两个条件,如果i是偶数,我们通过 continue 跳转到下一个迭代,而无需执行循环中 continue 之后的代码。当 i 等于 5 时,我们通过 break 停止迭代并退出循环。
module book::c_if {
fun main() {
let i = 0;
loop {
i = i + 1;
if (i / 2 == 0) continue;
if (i == 5) break;
// assume we do something here
};
std::debug::print<u8>(&i);
}
}
- 注意,如果 break 和 continue 是代码块中的最后一个关键字,则不能在其后加分号,因为后面的任何代码都不会被执行。请看这个例子
module book::c_if {
fun main() {
let i = 0;
loop {
i = i + 1;
if (i == 5) {
break; // will result in compiler error. correct is `break` without semi
// Error: Unreachable code
};
// same with continue here: no semi, never;
if (true) {
continue
};
// however you can put semi like this, because continue and break here
// are single expressions, hence they "end their own scope"
if (true) continue;
if (i == 5) break;
}
}
}
有条件退出 abort
有时,当某些条件失败时,您需要中止程序的执行。对于这种情况,Move 提供了有键字 abort。
module book::c_if {
fun main(a: u8) {
if (a != 10) {
abort 0;
}
// code here won't be executed if a != 10
// transaction aborted
}
}
- 关键字 abort 允许程序中止执行的同时报告错误代码。
使用 assert 内置方法
内置方法assert(<condition>, <code>)
对 abort和条件进行了封装,你可以在代码中任何地方使用它。
module book::c_if {
fun main(a: u8) {
assert(a == 10, 0);
// code here will be executed if (a == 10)
}
}
assert() 在不满足条件时将中止执行,在满足条件时将不执行任何操作。
事件 Event
- 事件是追踪链上动作的主要方式
/// Extended example of a shared object. Now with addition of events!
module examples::donuts_with_events {
use sui::transfer;
use sui::sui::SUI;
use sui::coin::{Self, Coin};
use sui::object::{Self, ID, UID};
use sui::balance::{Self, Balance};
use sui::tx_context::{Self, TxContext};
// This is the only dependency you need for events.
use sui::event;
/// For when Coin balance is too low.
const ENotEnough: u64 = 0;
/// Capability that grants an owner the right to collect profits.
struct ShopOwnerCap has key { id: UID }
/// A purchasable Donut. For simplicity's sake we ignore implementation.
struct Donut has key { id: UID }
struct DonutShop has key {
id: UID,
price: u64,
balance: Balance<SUI>
}
// ====== Events ======
/// For when someone has purchased a donut.
struct DonutBought has copy, drop {
id: ID
}
/// For when DonutShop owner has collected profits.
struct ProfitsCollected has copy, drop {
amount: u64
}
// ====== Functions ======
fun init(ctx: &mut TxContext) {
transfer::transfer(ShopOwnerCap {
id: object::new(ctx)
}, tx_context::sender(ctx));
transfer::share_object(DonutShop {
id: object::new(ctx),
price: 1000,
balance: balance::zero()
})
}
/// Buy a donut.
public entry fun buy_donut(
shop: &mut DonutShop, payment: &mut Coin<SUI>, ctx: &mut TxContext
) {
assert!(coin::value(payment) >= shop.price, ENotEnough);
let coin_balance = coin::balance_mut(payment);
let paid = balance::split(coin_balance, shop.price);
let id = object::new(ctx);
balance::join(&mut shop.balance, paid);
// Emit the event using future object's ID.
event::emit(DonutBought { id: object::uid_to_inner(&id) });
transfer::transfer(Donut { id }, tx_context::sender(ctx))
}
/// Consume donut and get nothing...
public entry fun eat_donut(d: Donut) {
let Donut { id } = d;
object::delete(id);
}
/// Take coin from `DonutShop` and transfer it to tx sender.
/// Requires authorization with `ShopOwnerCap`.
public entry fun collect_profits(
_: &ShopOwnerCap, shop: &mut DonutShop, ctx: &mut TxContext
) {
let amount = balance::value(&shop.balance);
let profits = coin::take(&mut shop.balance, amount, ctx);
// simply create new type instance and emit it
event::emit(ProfitsCollected { amount });
transfer::public_transfer(profits, tx_context::sender(ctx))
}
}
高级篇
结构体
结构体是自定义类型,它可以包含复杂数据, 也可以不包含任何数据。结构体由字段组成,可以简单地理解成"key-value"存储,其中 key 是字段的名称,而 value 是存储的内容。结构体使用关键字 struct 定义。
- 结构体是在 Move 中创建自定义类型的唯一方法。
定义
结构体只能在模块内部定义,并且以关键字 struct 开头:
struct NAME {
FIELD1: TYPE1,
FIELD2: TYPE2,
...
}
什么时候用结构体
简单数据类型 u8 - u256 bool不能满足,一个完整的表述的时候, 比如一个人,有身高,年龄,姓名,性别等等属性的时候, 把不同的数据类型复合成了一个复杂的数据结构来表示一个具体的东西
module sui::s {
use std::string::String;
struct Person{
name:String,
age:u64,
gender:u8,
height:u64
}
}
我们来看一些例子:
module book::m {
// struct can be without fields
// but it is a new type
struct Empty {}
struct MyStruct {
field1: address,
field2: bool,
field3: Empty
}
struct Example {
field1: u8,
field2: address,
field3: u64,
field4: bool,
field5: bool,
// you can use another struct as type
field6: MyStruct
}
}
一个结构体最多可以有 65535 个字段。
被定义的结构体会成为新的类型,可以通过定义它的模块访问此类型:
M::MyStruct;
// or
M::Example;
定义递归结构体
- 定义递归结构体 是不允许的。
Move 允许使用其它结构作为成员,但不能递归使用相同的结构体。Move 编译器会检查递归定义,不允许下面这样的代码:
module book::m {
struct MyStruct {
// WON'T COMPILE
field: MyStruct
}
}
创建结构体实例
- 要使用某结构体类型,需要先创建其实例。
可以用结构体的定义来创建实例,不同的是传入具体的值而不是类型。
module book::country {
struct Country {
id: u8,
population: u64
}
// Contry is a return type of this function!
public fun new_country(c_id: u8, c_population: u64): Country {
// structure creation is an expression
let country = Country {
id: c_id,
population: c_population
};
country
}
}
还可以通过传递与结构体的字段名匹配的变量名来简化创建新实例的代码。下面的 new_country() 函数中使用了这个简化方法:
// ...
public fun new_country(id: u8, population: u64): Country {
// id matches id: u8 field
// population matches population field
Country {
id,
population
}
// or even in one line: Country { id, population }
}
要创建一个空结构体(没有字段),只需使用花括号:
public fun empty(): Empty {
Empty {}
}
访问结构体成员字段
如果我们没有办法访问结构体的字段,那么它几乎是无用的。
只有在模块内才可以访问其结构体的字段。在模块之外,该结构体字段是不可见的。
结构字段仅在其模块内部可见。在此模块之外(在脚本或其他模块中),它只是一种类型。要访问结构的字段,请使用"."符号:
// ...
public fun get_country_population(country: Country): u64 {
country.population // <struct>.<property>
}
如果在同一模块中定义了嵌套结构类型,则可以用类似的方式对其进行访问,通常可以将其描述为:
<struct>.<field>
// and field can be another struct so
<struct>.<field>.<nested_struct_field>...
为结构体字段实现 getter 方法
为了使结构体字段在外部可读,需要实现一些方法,这些方法将读取这些字段并将它们作为返回值传递。通常,getter 方法的调用方式与结构体的字段相同,但是如果你的模块定义了多个结构体,则 getter 方法可能会带来不便。
module book::country {
struct Country {
id: u8,
population: u64
}
public fun new_country(id: u8, population: u64): Country {
Country {
id, population
}
}
// don't forget to make these methods public!
public fun id(country: &Country): u8 {
country.id
}
// don't mind ampersand here for now. you'll learn why it's
// put here in references chapter
public fun population(country: &Country): u64 {
country.population
}
// ... fun destroy ...
}
通过 getter 方法,我们允许模块的使用者访问结构体的字段:
module book::m {
use {{sender}}::Country as C;
use std::debug;
fun main() {
// variable here is of type C::Country
let country = C::new_country(1, 10000000);
debug::print<u8>(
&C::id(&country)
); // print id
debug::print<u64>(
&C::population(&country)
);
// however this is impossible and will lead to compile error
// let id = country.id;
// let population = country.population.
}
}
回收结构体
解构、或者销毁结构体需要使用语法 let <STRUCT DEF> = <STRUCT>
:
module book::country {
// ...
// we'll return values of this struct outside
public fun destroy(country: Country): (u8, u64) {
// variables must match struct fields
// all struct fields must be specified
let Country { id, population } = country;
// after destruction country is dropped
// but its fields are now variables and
// can be used
(id, population)
}
}
请注意,Move 中禁止定义不会被使用的变量,有时你可能需要在不使用其字段的情况下销毁该结构体。对于未使用的结构体字段,请使用下划线"_"表示
module book::country {
// ...
public fun destroy(country: Country) {
// this way you destroy struct and don't create unused variables
let Country { id: _, population: _ } = country;
// or take only id and don't init `population` variable
// let Country { id, population: _ } = country;
}
}
对象
- 要说明白Sui的对象模型得先从先从Sui的链上数据存储模型说起
- 对象在Sui上存储,维护了一个全局的Map数据结构 Map<ID,object>, 学过数据结构的都知道Map结构也可以理解成事HashMap的结构就是 key 是id,值是Object, id 肯定是唯一的,通过这个唯一的id 查找到object
如何定义一个对象
- 必须有 key 能力
- 必须第一个字段 是id,而且类型为 sui::object::UID
module book::obj {
use sui::object::UID;
struct Obj has key {
id:UID,
}
}
module book::obj {
use sui::object::UID;
struct ColorObject has key {
id: UID,
red: u8,
green: u8,
blue: u8,
}
}
代表ColorObject一个 Sui 对象类型,您可以使用它来创建最终可以存储在 Sui 网络上的 Sui 对象
- 这是会报错的
module book::obj {
struct Obj has key {
a:u64
}
}
- 这不是一个对象,只是一个普通数据结构
module book::obj {
struct Obj1 {
a:u64,
b:u64
}
struct Obj2 has store {
a:u64,
b:u64
}
}
UID 类型的说明
定义在Sui的标准库中,具体标准库是什么会在后面又详细的说明,我们一起看一下UID的定义
/// Sui object identifiers
module sui::object {
use std::bcs;
use sui::address;
use sui::tx_context::{Self, TxContext};
struct ID has copy, drop, store {
// We use `address` instead of `vector<u8>` here because `address` has a more
// compact serialization. `address` is serialized as a BCS fixed-length sequence,
// which saves us the length prefix we would pay for if this were `vector<u8>`.
// See https://github.com/diem/bcs#fixed-and-variable-length-sequences.
bytes: address
}
/// Globally unique IDs that define an object's ID in storage. Any Sui Object, that is a struct
/// with the `key` ability, must have `id: UID` as its first field.
/// These are globally unique in the sense that no two values of type `UID` are ever equal, in
/// other words for any two values `id1: UID` and `id2: UID`, `id1` != `id2`.
/// This is a privileged type that can only be derived from a `TxContext`.
/// `UID` doesn't have the `drop` ability, so deleting a `UID` requires a call to `delete`.
struct UID has store {
id: ID,
}
// === any object ===
/// Create a new object. Returns the `UID` that must be stored in a Sui object.
/// This is the only way to create `UID`s.
public fun new(ctx: &mut TxContext): UID {
UID {
id: ID { bytes: tx_context::fresh_object_address(ctx) },
}
}
public fun delete(id: UID) {
let UID { id: ID { bytes } } = id;
delete_impl(bytes)
}
}
创建 Sui 对象
严格来说,上面的代码只是定义好了 object 的模板,并不能说已经是object了,怎么理解这个呢 就像生产一个鼠标,我们只是做好了鼠标的模具,需要用这个模具来真正生产出具体的鼠标了, 这个鼠标才能算真正的对象,那么如何用我们定义好的对象模板生产出真正的对象, 其实如果你有其他编程语言基础的话你可能会很容易理解, 我们已经定义好了一个对象模板,需要生成一个对象实例,具体的代码如下
module book::obj {
use sui::object;
use sui::object::UID;
// tx_context::TxContext creates an alias to the TxContext struct in the tx_context module.
use sui::tx_context::TxContext;
struct ColorObject has key {
id: UID,
red: u8,
green: u8,
blue: u8,
}
fun new(red: u8, green: u8, blue: u8, ctx: &mut TxContext): ColorObject {
ColorObject {
id: object::new(ctx),
red,
green,
blue,
}
}
}
定义 Sui 对象类型模板后,您可以创建或实例化 Sui 对象。 要从其类型创建一个新的 Sui 对象,您必须为每个字段分配一个初始值, 包括id. 为 Sui 对象创建新对象的唯一方法UID是调用object::new. 该new函数将当前事务上下文作为生成唯一 ID 的参数
module book::obj {
use sui::object::UID;
use sui::transfer;
use sui::tx_context::{Self, TxContext};
use sui::object;
struct Obj has key {
id: UID,
}
fun init(ctx: &mut TxContext) {
let obj = Obj {
id: object::new(ctx)
};
transfer::transfer(obj, tx_context::sender(ctx))
}
}
正如代码所看到的,创建出来的实例,需要调用标准库的方法转移,这就是我们接下来需要讲到的 object owner模型 也叫对象所有权,但是在这之前 我们先看一下对象的内部其他信息
对象内部元数据
每个 Sui 对象都有以下元数据:
- 一个 32 字节的全局唯一 ID。对象 ID 源自创建对象的交易摘要和对交易生成的 ID 数量进行编码的计数器。
- 一个 8 字节无符号整数版本,随着每个读取或写入它的事务单调增加。
- 一个 32 字节的交易摘要,指示包含此对象作为输出的最后一笔交易。
- 一个 21 字节的所有者字段,指示如何访问此对象。对象所有权将在下一节中详细解释
对象所有权
每个对象都有一个所有者字段,指示如何拥有该对象。所有权决定了一个对象如何在事务中使用。有 4 种不同类型的所有权:
- 只读引用 ( &T)
- 可变引用 ( &mut T)
- 按值 ( T)
由一个地址拥有
这是 Move 对象最常见的情况。在 Move 代码中创建的 Move 对象可以转移到一个地址。转移后,该对象将归该地址所有。地址拥有的对象只能由该所有者地址签名的交易使用(即作为 Move 调用参数传递)。拥有的对象可以作为 Move 调用参数以 3 种形式中的任何一种传递:只读引用 ( &T)、可变引用 ( &mut T) 和按值 ( T)。重要的是要注意,即使一个对象通过只读引用传递(&T) 在 Move 调用中,仍然需要只有对象的所有者才能进行这样的调用。也就是说,在验证对象是否可以在交易中使用时,Move 调用的意图无关紧要,所有权才是最重要的。
module book::obj {
use sui::object::UID;
use sui::transfer;
use sui::tx_context::{Self, TxContext};
use sui::object;
struct Obj has key {
id: UID,
}
fun init(ctx: &mut TxContext) {
let obj = Obj {
id: object::new(ctx)
};
transfer::transfer(obj, tx_context::sender(ctx))
}
}
由另一个对象拥有
一个对象可以由另一个对象拥有。将这种直接所有权与对象包装区分开来很重要。 当您将一个对象的结构定义的字段设置为另一种对象类型时,可以将一个对象包装/嵌入到另一个对象中。例如:
module sui::obj_inner {
use sui::object::{UID};
use sui::tx_context::{TxContext, sender};
use sui::object;
use sui::transfer;
struct A has key,store{
id:UID
}
struct B has key {
id:UID,
a: A
}
fun init(ctx:& mut TxContext){
let a = A{
id:object::new(ctx)
};
let b = B{
id:object::new(ctx),
a
};
transfer::transfer(b,sender(ctx));
}
}
定义一个对象类型A,该类型包含一个类型为另一个对象类型的字段B。在这种情况下, type 的对象B被包装到 type 的对象中A。使用对象包装,包装的对象(在本例中为 object b)不会作为顶级对象存储在 Sui 存储中,并且无法通过对象 ID 访问它。相反,它只是 type 对象的序列化字节内容的一部分A。你可以把一个对象被包装的情况想象成类似于被删除的情况,只不过它的内容仍然存在于另一个对象的某个地方。
现在回到另一个对象拥有的对象的主题。当一个对象为另一个对象所有时,它不会被包装。这意味着子对象仍然作为顶级对象独立存在,可以直接在 Sui 存储中访问。所有权关系仅通过子对象的所有者字段进行跟踪。如果您仍想观察子对象或能够在其他事务中使用它,这将很有用。Sui 提供库 API 来使一个对象为另一个对象所拥有。有关如何执行此操作的更多详细信息,请参阅Sui Move 库。
不可变的常量
您不能改变不可变对象,并且不可变对象没有独占所有者。任何人都可以在 Sui Move 调用中使用不可变对象。
module book::obj {
use sui::object::UID;
use sui::transfer;
use sui::tx_context::{Self, TxContext};
use sui::object;
struct Obj has key {
id: UID,
}
fun init(ctx: &mut TxContext) {
let obj = Obj {
id: object::new(ctx)
};
transfer::freeze_object(obj);
}
}
所有 Sui Move 包都是不可变对象:您无法在发布后更改 Sui Move 包。您可以使用freeze_object操作将 Sui Move 对象转换为不可变对象。您只能在 Sui Move 调用中将不可变对象作为只读引用 ( &T) 传递。
共享 (这个不理解)
对象可以共享,这意味着任何人都可以读取或写入该对象。与可变拥有的对象(单写者)相比,共享对象需要共识来排序读取和写入。有关创建和访问共享对象的示例,请参阅共享对象。
在其他区块链中,每个对象都是共享的。但是,Sui 程序员通常可以选择使用共享对象、拥有对象或组合来实现特定用例。这种选择可能会对性能、安全性和实施复杂性产生影响。理解这些权衡的最好方法是查看一些以两种方式实现的用例示例:
托管:共享,拥有 拍卖:共享,拥有 井字游戏:共享,拥有
module book::obj {
use sui::object::UID;
use sui::transfer;
use sui::tx_context::{Self, TxContext};
use sui::object;
struct Obj has key {
id: UID,
}
fun init(ctx: &mut TxContext) {
let obj = Obj {
id: object::new(ctx)
};
transfer::share_object(obj);
}
}
module sui::transfer {
public fun transfer<T: key>(obj: T, recipient: address) {
transfer_impl(obj, recipient)
}
public fun public_transfer<T: key + store>(obj: T, recipient: address) {
transfer_impl(obj, recipient)
}
public fun freeze_object<T: key>(obj: T) {
freeze_object_impl(obj)
}
public fun public_freeze_object<T: key + store>(obj: T) {
freeze_object_impl(obj)
}
public fun share_object<T: key>(obj: T) {
share_object_impl(obj)
}
public fun public_share_object<T: key + store>(obj: T) {
share_object_impl(obj)
}
}
key + store
- 你会看到 关于所有权专业的方法其实有两组
- public_* 开头的函数 和没有 public的函数,那么什么时候使用 public_* 开头的函数呢
- 简单来说就是如果 定义的对象是 通用资产 就是 key + store 就用 public_*开头的函数
module book::obj {
use sui::object::UID;
use sui::transfer;
use sui::tx_context::{Self, TxContext};
use sui::object;
struct Obj has key , store {
id: UID,
}
fun init(ctx: &mut TxContext) {
let obj = Obj {
id: object::new(ctx)
};
transfer::public_transfer(obj, tx_context::sender(ctx))
}
}
详细讲解
https://examples.sui.io/basics/transfer.html
https://examples.sui.io/basics/custom-transfer.html
参考文档
https://docs.sui.io/build/programming-with-objects/ch1-object-basics
https://docs.sui.io/learn/objects
https://www.notion.so/706community/2-1-Object-3f2aeeefccf447d295375d803379f136?pvs=4
Abilities
Move 的类型系统非常灵活,每种类型都可以被四种限制符所修饰。这四种限制符我们称之为 abilities,它们定义了类型的值是否可以被复制、丢弃和存储。
这四种 abilities 限制符分别是:
- Copy,
- Drop,
- Store
- Key.
它们的功能分别是:
Copy - 被修饰的值可以被复制。
Drop - 被修饰的值在作用域结束时可以被丢弃。
Key - 被修饰的值可以作为键值对全局状态进行访问。
Store - 被修饰的值可以被存储到全局状态。
Abilities 语法
基本类型和内建类型的 abilities 是预先定义好的并且不可改变: integers, vector, addresses 和 boolean 类型的值先天具有 copy,drop 和 store ability。
然而,结构体的 ability 可以由开发者按照下面的语法进行添加:
struct NAME has ABILITY [, ABILITY] { [FIELDS] }
module book::library {
// each ability has matching keyword
// multiple abilities are listed with comma
struct Book has store, copy, drop {
year: u64
}
// single ability is also possible
struct Storage has key {
books: vector<Book>
}
// this one has no abilities
struct Empty {}
}
不带 Abilities 限制符的结构体
在进入 abilities 的具体用法之前, 我们不妨先来看一下,如果结构体不带任何 abilities 会发生什么?
module book::country {
struct Country {
id: u8,
population: u64
}
public fun new_country(id: u8, population: u64): Country {
Country { id, population }
}
}
module book::country_use {
use book::Country;
fun main() {
Country::new_country(1, 1000000);
}
}
Drop
按照 abilities 语法我们为这个结构体增加 drop ability,这个结构体的所有实例将可以被丢弃。
module book::country {
struct Country has drop { // has <ability>
id: u8,
population: u64
}
// ...
}
现在,Country 可以被丢弃了,代码也可以成功执行了。
script {
use {{sender}}::Country;
fun main() {
Country::new_country(1, 1000000); // value is dropped
}
}
- 注意 Destructuring 并不需要 Drop ability.
Copy
我们学习了如何创建一个结构体 Country 并在函数结束时丢弃它。但是如果我们想要复制一个结构体呢?缺省情况下结构体是按值传递的,制造一个结构体的副本需要借助关键字 copy (我们会在 下一章 更加深入的学习):
script {
use {{sender}}::Country;
fun main() {
let country = Country::new_country(1, 1000000);
let _ = copy country;
}
}
┌── scripts/main.move:6:17 ───
│
6 │ let _ = copy country;
│ ^^^^^^^^^^^^ Invalid 'copy' of owned value without the 'copy' ability
│
正如所料,缺少 copy ability 限制符的类型在进行复制时会报错:
module book::country {
struct Country has drop, copy { // see comma here!
id: u8,
population: u64
}
// ...
}
- 基本类型缺省具有 store, copy 和 drop 限制。
- 缺省情况下结构体不带任何限制符。
- Copy 和 Drop 限制符定义了一个值是否可以被复制和丢弃。
- 一个结构体有可能带有所有4种限制符。
泛型
泛型对于 Move 语言是必不可少的,它使得 Move 语言在区块链世界中如此独特,它是 Move 灵活性的重要来源。
首先,让我来引用 Rust Book 对于泛型得定义:泛型是具体类型或其他属性的抽象替代品。实际上,泛型允许我们只编写单个函数,而该函数可以应用于任何类型。这种函数也被称为模板 —— 一个可以应用于任何类型的模板处理程序。
Move 中泛型可以应用于结构体和函数的定义中。
结构体中的泛型
首先,我们将创建一个可容纳u64整型的 Box :
module book::generics {
struct Box {
value: u64
}
}
这个 Box 只能包含u64类型的值,这一点是非常清楚的。但是,如果我们想为u8类型或 bool类型创建相同的 Box 该怎么办呢?分别创建u8类型的 Box1 和bool型 Box2 吗?答案是否定的,因为可以使用泛型。
module book::generics {
struct Box<T> {
value: T
}
}
我们在结构体名字的后面增加。尖括号<…>里面用来定义泛型,这里T就是我们在结构体中模板化的类型。在结构体中,我们已经将T用作常规类型。类型T实际并不存在,它只是任何类型的占位符。
函数中的泛型
现在让我们为上面的结构体创建一个构造函数,该构造函数将首先使用u64类型。
module book::generics {
struct Box<T> {
value: T
}
// type u64 is put into angle brackets meaning
// that we're using Box with type u64
public fun create_box(value: u64): Box<u64> {
Box<u64>{ value }
}
}
带有泛型的结构体的创建稍微有些复杂,因为它们需要指定类型参数,需要把常规结构体 Box 变为 Box。Move没有任何限制什么类型可以被放进尖括号中。但是为了让create_box方法更通用,有没有更简单的方法?有的,在函数中使用泛型!
module book::generics {
// ...
public fun create_box<T>(value: T): Box<T> {
Box<T> { value }
}
// we'll get to this a bit later, trust me
public fun value<T: copy>(box: &Box<T>): T {
*&box.value
}
}
函数调用中使用泛型
上例中在定义函数时,我们像结构体一样在函数名之后添加了尖括号。如何使用它呢?就是在函数调用中指定类型。
module book::generics {
use {{sender}}::storage;
use std::debug;
fun main() {
// value will be of type storage::Box<bool>
let bool_box = storage::create_box<bool>(true);
let bool_val = storage::value(&bool_box);
assert(bool_val, 0);
// we can do the same with integer
let u64_box = storage::create_box<u64>(1000000);
let _ = storage::value(&u64_box);
// let's do the same with another box!
let u64_box_in_box = storage::create_box<storage::Box<u64>>(u64_box);
// accessing value of this box in box will be tricky :)
// Box<u64> is a type and Box<Box<u64>> is also a type
let value: u64 = storage::value<u64>(
&storage::value<storage::Box<u64>>( // Box<u64> type
&u64_box_in_box // Box<Box<u64>> type
)
);
// you've already seed debug::print<T> method
// which also uses generics to print any type
debug::print<u64>(&value);
}
}
这里我们用三种类型使用了 Box:bool, u64 和 Box。最后一个看起来有些复杂,但是一旦你习惯了,并且理解了泛型是如何工作的,它成为你日常工作的好帮手。
继续下一步之前,让我们做一个简单的回顾。我们通过将泛型添加到Box结构体中,使Box变得抽象了。与 Box 能提供的功能相比,它的定义相当简单。现在,我们可以使用任何类型创建Box,u64 或 address,甚至另一个 Box 或另一个结构体。
abilities 限制符
我们已经学习了 abilities,它们可以作为泛型的限制符来使用,限制符的名称和 ability 相同。
fun name<T: copy>() {} // allow only values that can be copied
fun name<T: copy + drop>() {} // values can be copied and dropped
fun name<T: key + store + drop + copy>() {} // all 4 abilities are present
也可以在结构体泛型参数中使用:
struct name<T: copy + drop> { value: T } // T can be copied and dropped
struct name<T: stored> { value: T } // T can be stored in global storage
- 请记住 + 这个语法符号,第一眼看上去可能不太适应,因为很少有语言在关键字列表中使用 +。
下面是一个使用限制符的例子:
module book::generics {
// contents of the box can be stored
struct Box<T: store> has key, store {
content: T
}
}
另一个需要被提及的是结构体的成员必须和结构体具有相同的 abilities (除了key以外)。这个很容易理解,如果结构体具有 copy ability,那么它的成员也必须能被 copy,否则结构体作为一个整体不能被 copy。Move 编译器允许代码不遵守这样的逻辑,但是运行时会出问题。
module book::generics {
// non-copyable or droppable struct
struct Error {}
// constraints are not specified
struct Box<T> has copy, drop {
contents: T
}
// this method creates box with non-copyable or droppable contents
public fun create_box(): Box<Error> {
Box { contents: Error {} }
}
}
这段代码可以成功编译和发布,但是如果你运行它就会出问题。
module book::generics {
fun main() {
{{sender}}::storage::create_box() // value is created and dropped
}
}
运行结果是报错 Box 不能被 drop。
┌── scripts/main.move:5:9 ───
│
5 │ storage::create_box();
│ ^^^^^^^^^^^^^^^^^^^^^ Cannot ignore values without the 'drop' ability. The value must be used
│
原因是创建结构体时所使用的成员值没有 drop ability。也就是 contents 不具备 Box 所要求的 abilities - copy 和 drop。
- 但是为了避免犯错,应该尽可能使泛型参数的限制符和结构体本身的 abilities 显式的保持一致。
所以下面这种定义的方法更安全:
// we add parent's constraints
// now inner type MUST be copyable and droppable
struct Box<T: copy + drop> has copy, drop {
contents: T
}
泛型中包含多个类型
我们也可以在泛型中使用多个类型,像使用单个类型一样,把多个类型放在尖括号中,并用逗号分隔。我们来试着添加一个新类型Shelf,它将容纳两个不同类型的Box。
module book::generics {
struct Box<T> {
value: T
}
struct Shelf<T1, T2> {
box_1: Box<T1>,
box_2: Box<T2>
}
public fun create_shelf<Type1, Type2>(
box_1: Box<Type1>,
box_2: Box<Type2>
): Shelf<Type1, Type2> {
Shelf {
box_1,
box_2
}
}
}
Shelf的类型参数需要与结构体字段定义中的类型顺序相匹配,而泛型中的类型参数的名称则无需相同,选择合适的名称即可。正是因为每种类型参数仅仅在其作用域范围内有效,所以无需使用相同的名字。
多类型泛型的使用与单类型泛型相同:
module book::generics {
use {{sender}}::storage;
fun main() {
let b1 = storage::create_box<u64>(100);
let b2 = storage::create_box<u64>(200);
// you can use any types - so same ones are also valid
let _ = storage::create_shelf<u64, u64>(b1, b2);
}
}
*你可以在函数或结构体定义中最多使用 18,446,744,073,709,551,615 (u64 最大值) 个泛型。你绝对不会达到此限制,因此可以随意使用。
未使用的类型参数
并非泛型中指定的每种类型参数都必须被使用。看这个例子:
module book::generics {
// these two types will be used to mark
// where box will be sent when it's taken from shelf
struct Abroad {}
struct Local {}
// modified Box will have target property
struct Box<T, Destination> {
value: T
}
public fun create_box<T, Dest>(value: T): Box<T, Dest> {
Box { value }
}
}
也可以在脚本中使用 :
module book::generics {
use {{sender}}::storage;
fun main() {
// value will be of type storage::Box<bool>
let _ = storage::create_box<bool, storage::Abroad>(true);
let _ = storage::create_box<u64, storage::Abroad>(1000);
let _ = storage::create_box<u128, storage::Local>(1000);
let _ = storage::create_box<address, storage::Local>(0x1);
// or even u64 destination!
let _ = storage::create_box<address, u64>(0x1);
}
}