前面已经实践过通过Aptos CLI编译、部署一个简单的Move合约到Aptos链上,接下来对Move编程语言进行基本用法的学习,学习内容参考The Move Book:aptos.dev/move/book/S…
本篇笔记不会完全记录Move语言的全部特性,而是选择其比较独特的部分进行重点记录。
更具体一点的学习,分为以下几部分:
- 基本数据类型
- 结构体与资源
- 全局存储
- 函数
基本数据类型
Move的基础类型有:
Integers, Bool, Address, Vector, Signer, References, Tuples and Unit
Integers
unsigned integer types: u8
, u16
, u32
, u64
, u128
,u256
Vector
vector<T>
是 Move 提供的唯一原始集合类型。
Signer
- Signer是Move的一个内建类型. Signer代表持有者代表一个地址行使权力的能力。
- 和Address类型比较起来,Address可以不经任何许可就创建出来,比如用Aptos CLI就可以创建任意的address,但是Signer不能通过字面指令创建,而是由虚拟机创建的。
- Signer的权限是被严格控制的,不可复制、存储,只有drop的能力。
- 用法举例:
script {
use std::signer;
fun main(s: signer) {
assert!(signer::address_of(&s) == @0x42, 0);
}
}
References
Move语言有两种类型的引用:mutable引用和immutable引用
-
mutable
- &mut:可变引用允许通过引用进行写入修改
-
immutable
- &:只读,不能修改引用对象的值
语法如下:
Syntax | Type | Description |
---|---|---|
&e | &T where e: T and T is a non-reference type | Create an immutable reference to e |
&mut e | &mut T where e: T and T is a non-reference type | Create a mutable reference to e . |
&e.f | &T where e.f: T | Create an immutable reference to field f of struct e . |
&mut e.f | &mut T where e.f: T | Create a mutable reference to field f of structe . |
freeze(e) | &T where e: &mut T | Convert the mutable reference e into an immutable reference. |
根据引用进行读写操作:
Syntax | Type | Description |
---|---|---|
*e | T where e is &T or &mut T | Read the value pointed to by e |
*e1 = e2 | () where e1: &mut T and e2: T | Update the value in e1 with e2. |
- 用到了类似C语言风格的 *语法,对mutable reference进行写入时,*e必须出现在等号的左边
- 可读的引用,必须有copy ability;可写的引用,必须有drop的ability
- reference是不能被存储的
Tuples和Unit
Tuple的出现是为了满足函数有多个返回值的需求,因此它只是出现在表达式中,换言之,它仅存在于源代码层面,并不存在于字节码层面。
- 只能出现在函数的表达式中,通常是在返回值表达式中
- 不能绑定到局部变量
- 不能在Struct中存储
- 不能用于实例化泛型
- unit()可以看做空元组,不产生任何运行值
- 适用于元祖的任何限制,也适用于unit
- 唯一可以对tuples执行的操作是解构destructuring,对于任何大小的元组,它们可以在
let
绑定或赋值中被解构。
Tuples的定义:
Syntax | Type | Description |
---|---|---|
() | (): () | Unit, the empty tuple, or the tuple of arity 0 |
(e1, ..., en) | (e1, ..., en): (T1, ..., Tn) where e_i: Ti s.t. 0 < i <= n and n > 0 | A n-tuple, a tuple of arity n, a tuple with n elements |
结构体与资源
Struct
- 结构体是包含类型字段的用户自定义数据结构,必须定义在Module内。
- 结构体可以存储任何非引用类型,包括其他结构体,但不能有递归用法,比如:struct Foo { x: Foo }这种就是错误的用法
- 结构体默认情况下是线性的、短暂的,但结合Abilities的使用可以扩展出更多的用法
Resource
- 如果结构体不能复制也不能删除,我们经常将它们称为资源
Move语言中的Abilities
-
[copy]
复制- 允许此类型的值被复制
-
[drop]
丢弃- 允许此类型的值被弹出/丢弃
-
[store]
存储- 允许此类型的值存在于全局存储的某个结构体中
-
[key]
键值- 允许此类型作为全局存储中的键(具有
key
能力的类型才能保存到全局存储中)
- 允许此类型作为全局存储中的键(具有
-
基础类型的abilities
bool
,u8
,u16
,u32
,u64
,u128
,u256
,address
都具备:copy
,drop
,store
的能力signer
只有drop
的能力,也就是说signer的值不能拷贝和存储- vector 有copy和drop的能力,是否有store的能力要看是哪种T
- 引用类型:&和&mut有copy和drop的能力,但没有store的能力
- 所有的基础类型都不具备key的能力
-
Struct的能力声明,用‘has’
-
例子:struct Pair has copy, drop, store { x: u64, y: u64 }
-
如果一个Struct声明自己有copy,drop,store的能力,那么它的每一个field都必定具备此能力
-
如果一个Struct声明自己有key的能力,它的每一个field一定具备store的能力,但不具备key的能力
-
全局存储
Aptos上采用的基于Move的存储结构和以太坊系所有的账本式记账方式的存储结构并不相同。在Aptos上,所有的资源都存储在账户(address)根节点下。每个根节点下可以存储资源resource和module代码,用伪代码表示可以是:
struct GlobalStorage {
resources: Map<(address, ResourceType), ResourceValue>
modules: Map<(address, ModuleName), ModuleBytecode>
}
全局存储的操作,有五种指令:
move_to<T>(&signer,T)
: 在 signer.address 下发布 Tmove_from<T>(address):T
:从 address 下删除 T 并返回Tborrow_global_mut<T>(address): &mut T
:返回 address 下 T 的可变引用borrow_global<T>(address): &T
:返回 address 下 T 的不可变引用exists<T>(address): bool
:返回 address 下是否存在T
Move对空指针的防范
Move在静态引用安全性方面有两个很重要的保障措施, 即通过禁止返回全局引用,以及需要使用 acquires 标注函数来防止空引用。
-
(1)函数的返回值不能是一个指向全局存储的引用
- 例子:当函数返回一个对全局存储的引用时,将无法编译通过
struct R has key { f: u64 } // will not compile fun ret_direct_resource_ref_bad(a: address): &R { borrow_global<R>(a) // error! } // also will not compile fun ret_resource_field_ref_bad(a: address): &u64 { &borrow_global<R>(a).f // error! }
-
(2)要求函数使用acquires标注,当且仅当以下情况出现时,函数必须要用aquires标注
- 函数体中包含:
move_from<T>
、borrow_global_mut<T>
或borrow_global<T>
指令 - 函数体中包含其他被 acquires标注的函数
- 函数体中包含:
函数
函数定义
fun <identifier><[type_parameters: constraint],*>([identifier: type],*): <return_type> <acquires [identifier],*> <function_body>
举例:
fun foo<T1, T2>(x: u64, y: T1, z: T2): (T2, T1, u64) { (z, y, x) }
函数可见性
-
Module中的函数,默认为private,只能在同module中被调用,不可被外部module和script调用
-
如果函数要被外部module和script调用,需要被定义为public或public(friend)
-
public修饰的函数可以被:(1)同module调用,(2)外部module调用,(3)外部scripte调用
-
public(friend)修饰的函数可以被:(1)同module调用,(2)friend list中声明的module调用
- 举例
address 0x42 { module m { friend 0x42::n; // friend declaration public(friend) fun foo(): u64 { 0 } fun calls_foo(): u64 { foo() } // valid } module n { fun calls_m_foo(): u64 { 0x42::m::foo() // valid } } module other { fun calls_m_foo(): u64 { 0x42::m::foo() // ERROR! // ^^^^^^^^^^^^ 'foo' can only be called from a 'friend' of module '0x42::m' } } } script { fun calls_m_foo(): u64 { 0x42::m::foo() // ERROR! // ^^^^^^^^^^^^ 'foo' can only be called from a 'friend' of module '0x42::m' } }
- friend用法:The
friend
syntax is used to declare modules that are trusted by the current module. A trusted module is allowed to call any function defined in the current module that have thepublic(friend)
visibility.
-
entry修饰符
- 旨在表明module的调用入口
- 被entry修饰的public函数可以安全地直接被链外的script脚本调用
- 可以修饰public函数,也可以修饰private函数
- 被entry修饰的函数可以接收String、vector等基础类型的变量,但不能有struct类型的,并且不能有任何返回值。
函数的返回值
- 函数体的最终计算结果,就是其返回值。当然,也可以用return显式声明
- 例如:
fun add(x: u64, y: u64): u64 {
x + y
}
小结
快速学习完The Move Book后,对Move语言的基本理解,可以概括为以下几个方面:
- Move是一种智能合约语言,目前有两个以高性能著称的L1链Aptos和SUI采用其作为智能合约语言。
- Move语言以Module的形式发布到链上,Module中包含了函数和自定义的结构体Struct,结构体由Field组成,Field可以是各种基础类型,比如Address,Bool,u8,u64等。
- 标记为entry的函数可以作为入口函数,public函数可以被外部的module或者脚本调用。函数和其他语言中的函数在使用方面没有太大的区别。
- 如果一个结构体不能被copy或drop,就可以称作资源,资源必须在函数体结束前转让所有权,这个特性使得它特别适合用来代表token。因为结构体默认情况下是线性的、短暂的,不能复制,不能丢弃,不能存储在全局存储中,为了方便拓展,Move定义了四种能力drop,copy,store,key用于扩展操作。