Skip to main content
在前一章中,我们掌握了Move的基础语法。现在,让我们进入Move语言最核心、最独特的部分——结构体(Structs)能力系统(Abilities) 如果说Move是一栋大楼,那么结构体就是砖块,能力系统就是粘合剂。这一章将教你如何用这些”砖块”和”粘合剂”构建出真正有用的”建筑”——数字资产。

3.1 结构体 (Structs)

结构体语法:字段定义与初始化

结构体是Move中定义自定义类型的方式。它允许你将多个字段组合在一起,形成一个新的数据类型。 基本语法:
module my_addr::struct_demo {
    // 定义一个User结构体
    struct User {
        id: u64,
        name: vector<u8>,
        age: u8,
        is_active: bool,
    }
}
创建结构体实例:
module my_addr::struct_demo {
    struct User {
        id: u64,
        name: vector<u8>,
        age: u8,
        is_active: bool,
    }
    
    fun create_user() {
        // 创建结构体实例 - 使用字段名: 值 的形式
        let alice = User {
            id: 1,
            name: b"Alice",
            age: 30,
            is_active: true,
        };
        
        // 访问字段
        let alice_age = alice.age;  // 30
        
        // 修改字段(需要mut)
        let mut bob = User {
            id: 2,
            name: b"Bob",
            age: 25,
            is_active: false,
        };
        
        bob.age = 26;  // 修改年龄
        bob.is_active = true;  // 激活账户
    }
}

Move 2 新特性:命名参数与位置参数

Move 2引入了灵活的结构体初始化方式,支持命名参数和位置参数。 命名参数(Named Arguments):
module my_addr::named_args {
    struct Point {
        x: u64,
        y: u64,
    }
    
    fun create_points() {
        // 传统方式:命名参数
        let p1 = Point { x: 10, y: 20 };
        
        // 可以调整字段顺序
        let p2 = Point { y: 30, x: 5 };  // 同样有效
    }
}
位置参数(Positional Arguments):
module my_addr::positional_args {
    struct Point {
        x: u64,
        y: u64,
    }
    
    fun create_points() {
        // 位置参数:按字段定义顺序传递
        let p1 = Point(10, 20);  // x=10, y=20
        
        // 错误示例:参数顺序错误
        // let p2 = Point(20, 10);  // x=20, y=10(这可能不是你想要的)
    }
}
混合使用(Move 2.1+):
module my_addr::mixed_args {
    struct User {
        id: u64,
        name: vector<u8>,
        age: u8,
        is_active: bool,
    }
    
    fun create_user() {
        // 混合参数:前两个使用位置参数,后两个使用命名参数
        let user1 = User(1, b"Alice", age: 30, is_active: true);
        
        // 也可以全部使用位置参数
        let user2 = User(2, b"Bob", 25, false);
        
        // 或者全部使用命名参数
        let user3 = User { 
            id: 3, 
            name: b"Charlie", 
            age: 35, 
            is_active: true 
        };
    }
}
🍳 厨师提示:
  • 推荐使用命名参数,因为它们更清晰、更安全(不会因顺序错误导致bug)
  • 位置参数适合简单的结构体(如只有2-3个字段)
  • 混合使用时,位置参数必须在前,且按顺序出现

类型参数:泛型结构体初步

泛型允许我们创建可重用的结构体,适用于多种类型。 基本泛型结构体:
module my_addr::generic_structs {
    // 定义一个泛型容器
    struct Container<T> {
        value: T,
    }
    
    // 多个类型参数
    struct Pair<T1, T2> {
        first: T1,
        second: T2,
    }
    
    fun use_generics() {
        // 指定具体类型
        let int_container = Container<u64> { value: 100 };
        let bool_container = Container<bool> { value: true };
        
        let pair = Pair<u64, vector<u8>> {
            first: 1,
            second: b"hello",
        };
    }
}
泛型与能力: 泛型结构体的能力取决于类型参数的能力。我们将在下一节详细讨论。
module my_addr::generic_with_abilities {
    // 限制T必须具有drop能力
    struct DisposableContainer<T: drop> has drop {
        item: T,
    }
    
    // 限制T必须具有copy和drop能力
    struct CopyableContainer<T: copy + drop> has copy, drop {
        item: T,
    }
}

3.2 四大能力 (Abilities) 深度解析

现在,我们进入Move最核心的部分——能力系统。能力是Move语言的”灵魂”,它决定了结构体可以做什么、不可以做什么。

什么是能力?

能力是编译时的类型特性,定义了类型的行为模式。Move有四种能力:
  1. copy:值可以被复制
  2. drop:值可以被丢弃
  3. store:值可以被存储在全局状态中
  4. key:值可以作为全局状态的键(即可以单独存储在账户下)
每个结构体可以有0到4种能力。能力组合决定了这个结构体代表什么。

copy能力:资源何时可以复制?

含义:具有copy能力的类型可以被复制。复制意味着创建值的精确副本。 适用场景:
  • 基本信息:如坐标、颜色、配置等
  • 可替代资产:如普通的Coin(一个单位的Coin和另一个单位完全一样)
误用风险: 如果资产被错误地标记为copy,任何人都可以无限复制它,导致通货膨胀。
module my_addr::copy_ability {
    // 具有copy能力的结构体
    struct Coordinate has copy, drop, store {
        x: u64,
        y: u64,
    }
    
    fun copy_demo() {
        let origin = Coordinate { x: 0, y: 0 };
        let copy1 = origin;  // 复制,origin仍然可用
        let copy2 = origin;  // 再次复制
        
        // 三个变量都是独立的副本
        let x1 = origin.x;   // 0
        let x2 = copy1.x;    // 0
        let x3 = copy2.x;    // 0
    }
    
    // 错误示例:尝试复制没有copy能力的结构体
    struct Token has store {
        id: u64,
    }
    
    fun error_demo() {
        let token = Token { id: 1 };
        // let token_copy = token;  // 编译错误!Token没有copy能力
        // 只能移动所有权,不能复制
    }
}

drop能力:资源何时可以被丢弃?

含义:具有drop能力的类型可以被丢弃(销毁)。当变量离开作用域时,会自动丢弃。 适用场景:
  • 临时数据:计算中间结果、临时配置
  • 可消费资产:如游戏中的一次性道具
资源生命周期: 没有drop能力的结构体必须在作用域结束前被显式处理(移动、存储等),不能自动丢弃。
module my_addr::drop_ability {
    // 具有drop能力的结构体
    struct TempData has drop {
        value: u64,
    }
    
    fun drop_demo() {
        let data = TempData { value: 100 };
        // 函数结束时,data被自动丢弃
        
        // 也可以显式丢弃
        let another = TempData { value: 200 };
        drop(another);  // 显式丢弃
    }
    
    // 错误示例:尝试丢弃没有drop能力的结构体
    struct Asset has store {
        id: u64,
    }
    
    fun error_demo() {
        let asset = Asset { id: 1 };
        // drop(asset);  // 编译错误!Asset没有drop能力
        
        // 函数结束时也会报错,因为asset没有drop能力,不能自动丢弃
        // 必须在作用域结束前处理asset
    }
}

key能力:资源何时可以作为索引?

含义:具有key能力的类型可以作为全局存储的键。这意味着它可以单独存储在账户下。 适用场景:
  • 账户级资源:每个账户只能有一个实例
  • 资产容器:如Vault、保险柜
重要特性: key能力通常与store能力一起使用。具有key能力的结构体可以通过move_to存储到账户下。
module my_addr::key_ability {
    // 具有key能力的结构体
    struct AccountVault has key {
        balance: u64,
        token_count: u64,
    }
    
    // 通常key和store一起使用
    struct UserProfile has key, store {
        name: vector<u8>,
        level: u8,
    }
}

store能力:资源何时可以被存储在全局状态?

含义:具有store能力的类型可以被存储在全局状态中,或者作为其他store结构体的字段。 适用场景:
  • 需要持久化的数据
  • 作为其他资源的一部分
嵌套存储规则: 如果一个结构体具有store能力,那么它的所有字段也必须具有store能力(或者是基本类型)。
module my_addr::store_ability {
    // 具有store能力的结构体
    struct Balance has store {
        amount: u64,
    }
    
    // 嵌套示例
    struct Account has key {
        balance: Balance,  // Balance有store能力,所以可以嵌套
        id: u64,           // u64有隐式的store能力
    }
    
    // 错误示例
    struct TempData has drop {  // 没有store能力
        temp: u64,
    }
    
    struct ErrorAccount has key {
        // data: TempData,  // 编译错误!TempData没有store能力
        id: u64,
    }
}

能力组合模式表

不同用途的结构体需要不同的能力组合。以下是常见的模式:
用途推荐能力组合示例解释
普通Coincopy + drop + storeaptos_framework::coin::Coin可替代资产,单位等价,可复制、可消费、可存储
NFT资产store + keyDigital Asset独一无二的资产,不可复制、不可丢弃,但可存储和转移
一次性凭证key投票凭证、门票不可复制、不可丢弃,只能转移
可丢弃资源drop临时配置、计算中间结果临时数据,用完即弃
普通数据copy + drop坐标、颜色、计数器可复制的临时数据
全局配置store全局参数、设置可存储但不可复制丢弃的数据
账户资源key + store账户余额、个人资料每个账户唯一的资源
基本类型的能力:
  • u8, u64, u128, u256: copy + drop + store
  • bool: copy + drop + store
  • address: copy + drop + store
  • vector<T>: 能力取决于T的能力
  • signer: 没有能力(特殊类型)
能力推导规则:
module my_addr::ability_rules {
    // 规则1:如果结构体声明了某种能力,其字段必须满足要求
    struct Container<T> has copy, drop, store {
        item: T,  // T必须具有copy, drop, store能力
    }
    
    // 规则2:能力可以继承
    struct Wrapper has copy, drop, store {
        // 因为u64具有copy, drop, store能力,所以Wrapper可以声明这些能力
        value: u64,
    }
    
    // 规则3:key能力是特殊的
    struct AccountResource has key {
        // key结构体的字段不需要特殊能力
        id: u64,           // 可以
        name: vector<u8>,  // 可以
    }
}

3.3 实战:定义你的第一个资产

现在,让我们动手定义几个具有不同能力组合的结构体,来模拟真实的资产类型。

设计一个”门票”结构体(不可复制、不可丢弃)

门票是一种典型的资源:每张门票都有唯一编号,不能复制(防止伪造),也不能随意丢弃(必须使用或转让)。
module my_addr::ticket_system {
    // 门票结构体:不可复制、不可丢弃,但可以存储和作为键
    struct Ticket has store, key {
        id: u64,           // 唯一ID
        event_name: vector<u8>,  // 活动名称
        owner: address,    // 当前拥有者
        used: bool,        // 是否已使用
        issue_time: u64,   // 发行时间
    }
    
    // 门票工厂(可复制、可丢弃)
    struct TicketFactory has copy, drop, store {
        event_name: vector<u8>,
        next_id: u64,
        price: u64,
    }
    
    // 创建新门票
    public fun create_ticket(
        factory: &mut TicketFactory,
        owner: address,
        current_time: u64
    ): Ticket {
        let ticket = Ticket {
            id: factory.next_id,
            event_name: copy factory.event_name,
            owner,
            used: false,
            issue_time: current_time,
        };
        
        factory.next_id = factory.next_id + 1;
        ticket
    }
    
    // 使用门票(标记为已使用)
    public fun use_ticket(ticket: &mut Ticket) {
        assert!(!ticket.used, 1001);  // 确保门票未使用
        ticket.used = true;
    }
    
    // 转移门票所有权
    public fun transfer_ticket(ticket: &mut Ticket, new_owner: address) {
        assert!(!ticket.used, 1002);  // 未使用的门票才能转移
        ticket.owner = new_owner;
    }
    
    // 注意:没有销毁门票的函数!
    // 因为Ticket没有drop能力,所以不能被丢弃
    // 门票要么被使用,要么永远存在
}

设计一个”积分”结构体(可复制、可丢弃)

积分是一种可替代、可消费的资源。它可以复制(因为单位等价),也可以丢弃(消费掉)。
module my_addr::points_system {
    // 积分结构体:可复制、可丢弃、可存储
    struct Points has copy, drop, store {
        amount: u64,
    }
    
    // 用户积分账户(不可复制、不可丢弃)
    struct PointsAccount has key {
        total_points: u64,
        last_updated: u64,
    }
    
    // 创建积分
    public fun mint_points(amount: u64): Points {
        Points { amount }
    }
    
    // 合并积分(消费旧的,创建新的)
    public fun merge_points(p1: Points, p2: Points): Points {
        Points { amount: p1.amount + p2.amount }
        // p1和p2在这里被自动丢弃(因为它们有drop能力)
    }
    
    // 分割积分
    public fun split_points(p: Points, amount: u64): (Points, Points) {
        assert!(p.amount >= amount, 1001);
        
        let first = Points { amount };
        let second = Points { amount: p.amount - amount };
        
        (first, second)
        // 原始p被自动丢弃(因为已经"移动"出了这个函数)
    }
    
    // 复制积分(演示copy能力)
    public fun duplicate_points(p: Points): (Points, Points) {
        // 因为Points有copy能力,所以可以直接复制
        (p, p)
    }
    
    // 消费积分
    public fun spend_points(p: Points, cost: u64): Points {
        assert!(p.amount >= cost, 1002);
        
        // 返回剩余积分
        Points { amount: p.amount - cost }
        // 原始p被丢弃,新创建的Points被返回
    }
}

常见错误:能力缺失导致的编译错误分析

在Move开发中,能力相关的错误非常常见。让我们分析几个典型错误: 错误1:尝试复制没有copy能力的结构体
module my_addr::error1 {
    struct Asset has store, key {
        id: u64,
    }
    
    fun test() {
        let asset = Asset { id: 1 };
        // let asset2 = asset;  // 编译错误!
        // ^ 错误信息:cannot copy value without 'copy' ability
        
        // 正确做法:移动所有权
        let asset2 = asset;  // 移动,asset不再可用
        // 或者为Asset添加copy能力(如果不应该复制,则不要添加)
    }
}
错误2:函数结束时自动丢弃没有drop能力的值
module my_addr::error2 {
    struct Asset has store, key {
        id: u64,
    }
    
    fun create_and_drop() {
        let asset = Asset { id: 1 };
        // 函数结束时报错:cannot ignore value without 'drop' ability
        // 必须显式处理这个值(存储、移动等)
    }
    
    fun correct_way() {
        let asset = Asset { id: 1 };
        
        // 方法1:存储到全局状态(第4章会学)
        // move_to<Asset>(sender, asset);
        
        // 方法2:返回给调用者
        // asset  // 作为返回值
        
        // 方法3:传递给其他函数
        // consume_asset(asset);
    }
}
错误3:在需要store的地方使用没有store能力的类型
module my_addr::error3 {
    struct TempData has drop {  // 没有store能力
        value: u64,
    }
    
    struct Container has store {
        // data: TempData,  // 编译错误!
        // ^ TempData does not have 'store' ability
        
        id: u64,  // 正确:u64有隐式store能力
    }
}
错误4:能力约束不满足
module my_addr::error4 {
    struct Container<T> has copy, drop, store {
        item: T,
    }
    
    struct Asset has store, key {  // 没有copy和drop能力
        id: u64,
    }
    
    fun test() {
        // let c = Container<Asset> { item: Asset { id: 1 } };
        // 编译错误:Asset does not have 'copy' ability
        // 因为Container声明了copy能力,所以T必须具有copy能力
    }
}
错误5:混淆&TT的能力
module my_addr::error5 {
    struct Asset has key {
        id: u64,
    }
    
    fun test() {
        // 引用有隐式的copy和drop能力,即使指向的类型没有
        let asset_ref: &Asset = ...;
        let another_ref = asset_ref;  // 可以复制引用
        // 但引用的内容(Asset本身)仍然不能复制
        
        // 错误:尝试通过引用复制内容
        // let asset_copy = *asset_ref;  // 编译错误!
    }
}
调试技巧:
  1. 仔细阅读错误信息:Move编译器会明确指出缺少哪种能力
  2. 思考数据的本质:这个数据代表什么?它应该可以被复制吗?可以被丢弃吗?
  3. 检查所有字段:结构体的能力取决于其字段的能力
  4. 使用类型约束:泛型函数和结构体可以指定能力约束
// 正确示例:显式指定能力约束
module my_addr::correct_generic {
    // 只接受具有copy和drop能力的T
    struct CopyableContainer<T: copy + drop> has copy, drop, store {
        item: T,
    }
    
    // 接受任何具有store能力的T
    struct StoreContainer<T: store> has store {
        item: T,
    }
    
    // 接受任何类型(无约束)
    struct AnyContainer<T> has store {
        item: T,
    }
}

📝 本章总结

恭喜!你已经掌握了Move语言最核心、最独特的部分: 结构体定义:学会了如何定义和使用结构体,包括泛型结构体
能力系统:深入理解了四大能力(copy、drop、store、key)的含义和用途
能力组合:掌握了不同场景下的能力组合模式
实战应用:设计了门票和积分两种不同类型的资产
错误处理:学会了分析和解决能力相关的编译错误
关键收获:
  1. 能力是编译期概念:它们在编译时检查,确保代码的安全性和正确性
  2. 安全源于约束:Move通过限制能力来防止常见漏洞(如资产无限增发、意外销毁)
  3. 设计决定能力:在设计结构体时,首先要思考”这个数据代表什么?“,然后选择合适的能力组合
核心原则:
  • 资产通常没有copy能力(防止复制)
  • 资产通常没有drop能力(防止意外销毁)
  • 账户资源需要key能力(存储在账户下)
  • 可存储数据需要store能力(保存在链上)
下一步挑战:
  1. 尝试设计一个”会员卡”结构体,应该有哪些能力?
  2. 设计一个”游戏道具”结构体,有哪些能力?
  3. 思考:为什么Aptos上的NFT标准(Digital Asset)使用store + key而不是store + key + copy + drop
🧑‍🍳 厨师进阶思考:
  • 如果一个结构体只有key能力,没有store能力,会发生什么?
  • 如果一个结构体有copy能力但没有drop能力,它可以用在哪里?
  • 如何设计一个”租赁”系统,其中资产可以在一定时间后自动归还?
在下一章,我们将学习如何将这些结构体存储到区块链上,并对其进行操作。我们将进入第三部分:烹饪技巧,学习Move的链上核心机制。
👨‍🍳 厨师笔记:
当我第一次理解能力系统时,我感觉自己像发现了新大陆。原来编程语言可以如此优雅地表达资产的性质!在传统语言中,我们需要在运行时检查”这个资产可以复制吗?“,而在Move中,这变成了编译时的类型检查。
记住这个比喻:能力就像现实世界中的物理定律。钞票不能复制(copy),不能凭空消失(drop),必须放在某个地方(store),而且你只能从自己的钱包里拿出来(key)。Move只是把这些定律变成了代码。
如果你觉得能力系统有些复杂,别担心!这是正常的。多练习,多思考每个结构体应该代表什么,能力选择会变得越来越自然。很快你就会发现,这种”约束”其实是一种”解放”——它让你写出了更安全的代码。
准备好进入下一章了吗?让我们继续前进,学习如何操作链上资源! 🔥