IOTA 学习笔记(六):Move 语言入门
上一期我们专门讲了 IOTA 的对象模型。对象模型是理解当前 IOTA 的关键,因为在 IOTA 中,链上状态不是简单挂在账户下面的一组变量,而是由一个个对象组成。每个对象都有自己的 ID、类型、所有者、版本和数据。
从这一期开始,我们进入 Move 语言。
如果说对象模型回答的是“链上状态如何存在”,那么 Move 语言回答的就是“链上状态如何被定义和操作”。在 IOTA 中,开发者通过 Move 编写智能合约,定义对象结构,编写对象创建、修改、转移和销毁的逻辑。
这一期不会追求把 Move 的所有语法一次讲完,而是先从 IOTA 开发最常用、最基础的几个概念入手:module、struct、function、ability、UID、TxContext 和 transfer。理解这些概念后,后面再写 Counter 合约、发布 package、调用函数,就会顺畅很多。
1. Move 是什么
Move 是一种面向区块链智能合约的编程语言。它最初被设计出来,就是为了更安全地表达链上资产、资源和状态变化。
很多传统编程语言关注的是通用计算,比如怎么处理字符串、数组、文件、网络请求。而 Move 更关注链上资产的安全管理,比如:
一个对象能不能被复制? 一个对象能不能被丢弃? 一个对象能不能被转移? 谁有权限修改这个对象? 这个对象能不能成为链上状态?这些问题在普通程序里也许不是核心问题,但在区块链里非常重要。
因为链上资产一旦可以被随意复制,就会产生严重问题。比如一枚代币、一个 NFT、一个权限凭证,如果能像普通变量一样随便复制,就会破坏资产稀缺性和所有权。因此,Move 从语言层面就对资源、对象和所有权进行了更严格的约束。
可以简单理解为:
Move 是一种围绕链上资产和对象安全设计的智能合约语言。
在 IOTA 中,Move 和对象模型结合得非常紧密。开发者通过 Move 定义对象类型,通过函数创建对象、修改对象、转移对象,并通过交易把这些操作提交到链上执行。
2. 为什么 IOTA 选择 Move
理解 IOTA Move,不能只从“语法”角度看,还要从“对象模型”角度看。
当前 IOTA 的链上状态以对象为中心。对象有 ID,有所有者,有版本,也可以被转移或共享。而 Move 天然适合描述这种对象化状态。
例如,一个 Counter 对象可以用 Move 结构体表示:
public struct Counter has key { id: UID, value: u64, }这里的Counter不是一个普通临时变量,而是可以成为链上对象的数据结构。它有唯一 ID,也有自己的状态字段value。
再比如,一个函数可以创建这个对象:
public fun create(ctx: &mut TxContext) { let counter = Counter { id: object::new(ctx), value: 0, }; transfer::transfer(counter, tx_context::sender(ctx)); }这段代码大致表达了三件事:
创建一个 Counter 对象 给它分配唯一 ID 把它转移给交易发送者这正好对应 IOTA 对象模型中的几个核心动作:创建对象、赋予对象身份、设置对象所有者。
所以,IOTA 选择 Move 的重要原因之一,是 Move 能够比较自然地表达链上对象、资产和所有权变化。
3. Move Package:合约代码的组织单位
在 IOTA 中,Move 合约通常以 package 的形式组织。
一个 package 可以理解为一个合约项目。它里面可以包含多个 Move 源文件,也可以包含多个 module。发布到链上之后,package 会成为链上的 Package 对象,并拥有自己的 Package ID。
可以简单表示为:
Move Package ├── Move.toml └── sources/ ├── counter.move └── another_module.move其中,Move.toml是 package 的配置文件,sources目录存放 Move 源码。
发布之后,链上会生成一个 Package ID。后面调用这个 package 里的函数时,通常需要使用类似下面的路径:
PackageID::ModuleName::FunctionName例如:
0xabc...::counter::create 0xabc...::counter::increment这里:
0xabc... 是 Package ID counter 是模块名 create / increment 是函数名所以,package 是 Move 合约的项目单位,module 是代码的组织单位,function 是具体可执行逻辑。
4. Module:Move 代码的基本模块
Move 中的 module 可以理解为合约模块。
一个 module 里面可以定义结构体、函数和常量。它类似于其他语言中的模块或命名空间,用来组织一组相关逻辑。
例如:
module hello::counter { use iota::object::{Self, UID}; use iota::transfer; use iota::tx_context::{Self, TxContext}; public struct Counter has key { id: UID, value: u64, } public fun create(ctx: &mut TxContext) { let counter = Counter { id: object::new(ctx), value: 0, }; transfer::transfer(counter, tx_context::sender(ctx)); } }这个例子里,hello::counter就是模块名。
可以把它拆开理解:
hello:package 地址或命名空间 counter:模块名在这个模块中,我们定义了一个Counter结构体,还定义了一个create函数。
初学时可以把 module 理解为:
module 是一组相关链上对象和操作函数的集合。
例如,Counter 模块负责计数器对象;Token 模块负责代币对象;Marketplace 模块负责市场对象;Voting 模块负责投票对象。
5. Struct:定义链上对象的数据结构
Move 中的 struct 用来定义数据结构。
在 IOTA Move 中,struct 经常用来定义链上对象。例如:
public struct Counter has key { id: UID, value: u64, }这段代码定义了一个 Counter 结构体,它包含两个字段:
id: UID value: u64其中,value: u64很容易理解,它表示一个无符号 64 位整数,用来保存计数值。
关键是id: UID。
UID可以理解为链上对象的唯一身份。一个 struct 如果要成为链上对象,通常需要有id: UID字段。这样链上系统才能给它分配唯一 Object ID,并追踪它的所有权和版本变化。
另外,has key也很重要。
key是 Move 中的一种 ability。一个 struct 如果有keyability,就表示它可以作为链上对象存在。
因此,这个 Counter 可以成为链上对象,是因为它满足两个关键条件:
具有 key ability 包含 id: UID 字段可以简单理解为:
struct 定义对象长什么样,UID 赋予对象唯一身份,key ability 让它可以成为链上对象。
6. Ability:Move 如何约束资源行为
Move 中有一个非常重要的概念,叫 ability。
ability 可以理解为结构体具备的能力。它决定这个结构体的值能不能被复制、丢弃、存储或作为链上对象。
常见 ability 包括:
copy drop store key它们大致可以这样理解。
copy表示这个值可以被复制。
如果一个类型有 copy ability,那么它可以像普通数字一样复制。
drop表示这个值可以被丢弃。
如果一个类型有 drop ability,那么它不被使用时可以直接丢掉。
store表示这个值可以被存储到其他结构体中。
如果一个类型有 store ability,它可以作为字段被放入其他对象里。
key表示这个类型可以作为链上对象。
如果一个 struct 有 key ability,它就可以拥有全局唯一 ID,并作为对象存储在链上。
Move 之所以安全,很大程度上就来自这些 ability 约束。
例如,普通数字u64可以复制和丢弃,这没有问题。
但一个链上资产对象不能随便复制,也不能随便丢弃。否则代币、NFT、权限凭证等资产就会失去安全性。
所以,Move 不会默认让所有类型都具备所有能力。开发者必须明确声明这个 struct 有哪些 ability。
例如:
public struct Counter has key { id: UID, value: u64, }这里 Counter 只有keyability,说明它可以成为链上对象,但不表示它可以被随便复制或丢弃。
这就是 Move 和普通编程语言很不一样的地方。
7. Function:定义对象如何被操作
Move 中的 function 用来定义可以执行的逻辑。
在 IOTA 合约里,函数通常围绕对象展开,比如创建对象、读取对象、修改对象、转移对象。
例如创建 Counter:
public fun create(ctx: &mut TxContext) { let counter = Counter { id: object::new(ctx), value: 0, }; transfer::transfer(counter, tx_context::sender(ctx)); }这个函数的作用是创建一个 Counter 对象,并把它转移给交易发送者。
再比如修改 Counter:
public fun increment(counter: &mut Counter) { counter.value = counter.value + 1; }这个函数的作用是把 Counter 的 value 加 1。
这里有一个很重要的参数:
counter: &mut Counter它表示函数接收一个 Counter 的可变引用。也就是说,这个函数不会拿走 Counter 对象本身,而是借用它并修改它。
可以简单理解为:
Counter:直接传入对象本身 &Counter:不可变引用,只读 &mut Counter:可变引用,可修改这和对象操作关系很密切。
如果一个函数只需要查看对象,可以使用不可变引用。
如果一个函数需要修改对象,就需要可变引用。
如果一个函数要转移或销毁对象,可能需要直接接收对象本身。
所以,学习 Move 函数时,不只要看函数名,还要看参数形式。
8. public、entry 和可调用性
Move 函数前面经常会出现public或entry。
public表示这个函数可以被其他模块调用。entry表示这个函数可以作为交易入口被直接调用。
例如:
public entry fun create(ctx: &mut TxContext) { // ... }这个函数既是 public,又是 entry。它可以被外部调用,也可以作为交易入口。
再比如:
public fun value(counter: &Counter): u64 { counter.value }这个函数是 public,但不一定是 entry。它可以被其他 Move 代码调用,用来读取 Counter 的值。
初学时可以先这样理解:
public:其他模块能不能调用 entry:用户能不能通过交易直接调用在实际合约开发中,哪些函数应该设为 entry,哪些函数只作为内部辅助函数,需要谨慎设计。
例如,创建对象、转账、提交操作这类用户直接触发的函数,通常适合作为 entry。
而一些内部计算函数、权限检查函数、辅助函数,则不一定需要 entry。
9. TxContext:交易上下文
在 Move 合约里,经常会看到TxContext。
例如:
public fun create(ctx: &mut TxContext) { let counter = Counter { id: object::new(ctx), value: 0, }; transfer::transfer(counter, tx_context::sender(ctx)); }TxContext可以理解为交易上下文。它提供了当前交易相关的信息,也可以用于创建新的对象 ID。
常见用途包括:
获取交易发送者 创建新的对象 UID 访问当前交易相关信息例如:
tx_context::sender(ctx)表示获取当前交易的发送者地址。
再例如:
object::new(ctx)表示基于当前交易上下文创建一个新的 UID。
为什么创建对象需要 TxContext?
因为链上对象需要唯一 ID,而这个 ID 必须由链上系统按照规则生成。TxContext 就是生成新对象身份时需要用到的上下文。
可以简单理解为:
TxContext 是 Move 函数和当前交易之间的桥梁。创建对象、获取发送者等操作,都需要通过它完成。
10. transfer:对象如何转移给用户
创建对象之后,对象需要有所有者。
在 IOTA Move 中,经常会使用transfer::transfer把对象转移给某个地址。
例如:
transfer::transfer(counter, tx_context::sender(ctx));这句代码的意思是:
把 counter 对象转移给当前交易发送者如果没有这一步,对象创建出来后就没有被正确交给用户。
所以,一个典型的对象创建函数通常包含三步:
定义对象内容 创建对象 UID 转移对象给某个地址对应代码就是:
let counter = Counter { id: object::new(ctx), value: 0, }; transfer::transfer(counter, tx_context::sender(ctx));从对象模型角度看,这段代码完成了:
新建 Counter 对象 生成 Object ID 设置初始 value 把 owner 设置为交易发送者这就是 Move 和对象模型结合的一个典型例子。
11. 一个最小 Counter 合约示例
现在可以把前面的概念串起来,看一个最小 Counter 合约。
module hello::counter { use iota::object::{Self, UID}; use iota::transfer; use iota::tx_context::{Self, TxContext}; public struct Counter has key { id: UID, value: u64, } public entry fun create(ctx: &mut TxContext) { let counter = Counter { id: object::new(ctx), value: 0, }; transfer::transfer(counter, tx_context::sender(ctx)); } public entry fun increment(counter: &mut Counter) { counter.value = counter.value + 1; } public fun value(counter: &Counter): u64 { counter.value } }这个合约虽然很短,但已经包含了 IOTA Move 入门中最关键的几个概念。
module hello::counter表示这是一个名为 counter 的模块。use用来引入外部模块。Counter has key表示 Counter 可以成为链上对象。id: UID表示 Counter 有唯一对象身份。value: u64表示 Counter 内部保存计数值。create用来创建 Counter 对象。increment用来修改 Counter 对象。value用来读取 Counter 的数值。TxContext用来创建对象和获取交易发送者。transfer::transfer用来把对象转移给用户。
这个例子非常适合初学者,因为它展示了一条完整链路:
定义对象 → 创建对象 → 转移对象 → 修改对象 → 读取对象后面真正上手 IOTA CLI 时,我们就可以围绕这个 Counter 合约进行构建、发布和调用。
12. Move 和普通智能合约语言的差异
如果之前接触过 Solidity,学习 Move 时会感觉它的思维方式不太一样。
Solidity 中,合约通常有一个合约地址,状态变量存放在合约存储里。用户调用合约函数,合约内部状态发生变化。
可以简单理解为:
Solidity: 合约地址 → 合约存储 → 函数修改存储变量Move 和 IOTA 对象模型则更强调对象:
IOTA Move: Package 定义逻辑 Object 保存状态 函数操作对象 对象有所有者和版本也就是说,在 IOTA Move 中,合约代码和链上状态并不是简单绑定在一个合约账户里。Package 更像代码,Object 更像状态。函数通过参数接收对象,然后根据权限和类型规则操作这些对象。
这会带来一种新的开发思维:
不是先问“合约里有哪些变量” 而是先问“链上有哪些对象” 不是只问“函数改了哪个状态变量” 而是问“函数接收了哪些对象、修改了哪些对象、转移了哪些对象”这就是 Move 和对象模型结合后的核心思路。
13. 初学者容易混淆的几个问题
第一个问题:struct 就一定是链上对象吗?
不是。struct 只是数据结构。只有具备相应 ability,并符合对象要求的 struct,才可以成为链上对象。通常,具有keyability 且包含id: UID的 struct,才是链上对象类型。
第二个问题:UID 和 Object ID 是什么关系?
UID 可以理解为对象在 Move 代码中的唯一身份字段。对象发布到链上后,会对应一个可查询的 Object ID。入门阶段可以把 UID 理解为对象 ID 的来源。
第三个问题:为什么创建对象需要 TxContext?
因为新对象需要唯一 ID,而这个 ID 需要基于当前交易上下文创建。TxContext 提供了创建 UID 和获取交易发送者等能力。
第四个问题:public 和 entry 是不是一样?
不是。public 表示函数能被其他模块调用,entry 表示函数能作为交易入口被直接调用。一个函数可以既是 public 又是 entry,也可以只是 public。
第五个问题:为什么有的函数参数是Counter,有的是&Counter,有的是&mut Counter?
这表示对象传入方式不同。Counter表示传入对象本身,&Counter表示只读引用,&mut Counter表示可变引用。是否需要修改对象,决定了应该使用哪种方式。
第六个问题:Move 合约是不是只要写函数就行?
不是。Move 合约最重要的是定义对象和对象操作规则。函数只是操作对象的入口,真正关键的是对象类型、所有权、权限和状态变化。
14. 学习 Move 的正确顺序
初学 IOTA Move,不建议一开始就看复杂项目。更好的顺序是:
第一,先理解对象模型。
知道 Object、Owner、UID、Package、Coin、Shared Object 是什么。
第二,学习最小 Move 语法。
理解 module、use、struct、function、ability、TxContext。
第三,写一个 Counter 合约。
通过最简单的对象创建和修改,理解 Move 合约的基本流程。
第四,学习发布 package。
知道 Move 代码如何构建、如何发布到链上,Package ID 是什么。
第五,学习调用函数。
通过 CLI 调用 create、increment 等函数,观察对象状态变化。
第六,学习 PTB。
理解如何在一笔交易里组合多个操作。
第七,再学习复杂对象。
例如共享对象、动态字段、代币、NFT、权限控制、package 升级等。
这个顺序比较适合初学者,不会一开始就被复杂概念淹没。
15. 小结
这一期主要介绍了 IOTA Move 的基础概念。
Move 是一种围绕链上资产和对象安全设计的智能合约语言。它和 IOTA 的对象模型结合得非常紧密。开发者通过 Move 定义对象结构,通过函数创建、修改和转移对象。
本期重点概念包括:
Package:Move 合约项目,发布后成为链上 Package 对象 Module:代码模块,用于组织结构体和函数 Struct:数据结构,可以定义链上对象 Ability:约束类型能否复制、丢弃、存储或成为对象 UID:对象唯一身份 TxContext:交易上下文,用于创建对象和获取发送者 Function:对象操作逻辑 Transfer:对象所有权转移可以用一句话总结:
在 IOTA Move 中,Package 定义逻辑,Object 保存状态,Function 操作对象,TxContext 连接交易与对象创建。
下一期,我们会进入 IOTA CLI 基础操作,学习如何配置网络环境、查看地址、查看余额、查询对象,为后面本地部署和合约调用做准备。
