返回 SC 笔记
SC Day 11

Rust - enum + 模式匹配(match) + Option/Result

### 1. Rust enum:不只是枚举,而是代数数据类型

2026-04-11
第一阶段:基础构建
rustenumpattern-matchingoptionresulterror-handling

日期: 2026-04-11 方向: Rust 阶段: 第一阶段:基础构建 标签: #rust #enum #pattern-matching #option #result #error-handling


今日目标

类型内容
学习掌握 Rust enum 的强大表达力、模式匹配的穷尽性检查、Option/Result 的错误处理哲学
实操实现交易状态枚举、用 Option/Result 处理链上查询的各种边界情况
产出完整的交易状态机 + 错误处理模式代码 + 与 Solidity enum 的对比分析

核心概念

1. Rust enum:不只是枚举,而是代数数据类型

Rust 的 enum 远不止 Solidity 那种简单的整数标签。它是真正的代数数据类型 (Algebraic Data Type, ADT),每个变体 (variant) 可以携带不同类型和数量的数据。这种设计在区块链开发中极其有用——交易状态、执行结果、事件类型天然适合用 enum 建模。

// Solidity 中的 enum 只是简单的整数标签
// enum Status { Pending, Confirmed, Failed }
// 它本质上就是 uint8,不能携带任何数据

// Rust 的 enum 可以让每个变体携带不同的数据
enum TransactionStatus {
    // 无数据变体(类似 Solidity 的 enum)
    Pending,

    // 元组变体:携带匿名字段
    Confirmed(u64),  // 确认区块高度

    // 结构体变体:携带命名字段
    Failed {
        error_code: u32,
        message: String,
    },

    // 携带复杂数据
    Reverted {
        reason: String,
        gas_used: u64,
        revert_data: Vec<u8>,
    },
}

这种能力意味着你可以把状态和状态相关的数据绑定在一起,避免了 Solidity 中常见的 "状态字段 + 多个可选字段" 的冗余模式。

2. match:穷尽性模式匹配

match 是 Rust 处理 enum 的核心武器。编译器会强制你处理所有变体,这在区块链场景中至关重要——遗漏一个交易状态可能导致资金丢失。

fn describe_tx(status: &TransactionStatus) -> String {
    match status {
        TransactionStatus::Pending => {
            "交易待处理,等待矿工打包".to_string()
        }
        TransactionStatus::Confirmed(block) => {
            format!("交易已确认,区块高度: {}", block)
        }
        TransactionStatus::Failed { error_code, message } => {
            format!("交易失败 [错误码: {}]: {}", error_code, message)
        }
        TransactionStatus::Reverted { reason, gas_used, .. } => {
            // 用 .. 忽略不需要的字段
            format!("交易回滚: {},消耗 Gas: {}", reason, gas_used)
        }
    }
}

match 的高级模式

fn classify_gas(gas_price_gwei: u64) -> &'static str {
    match gas_price_gwei {
        // 精确匹配
        0 => "无效的 Gas 价格",

        // 范围匹配
        1..=10 => "低 Gas(适合不急的交易)",
        11..=50 => "正常 Gas",
        51..=200 => "高 Gas(网络拥堵)",

        // 守卫条件(guard)
        g if g > 200 && g <= 1000 => "极高 Gas(谨慎操作)",

        // 通配符:兜底所有其他情况
        _ => "异常 Gas 价格",
    }
}

// 多值匹配
fn is_erc_standard(standard: &str) -> bool {
    match standard {
        "ERC20" | "ERC721" | "ERC1155" | "ERC4626" => true,
        _ => false,
    }
}

解构嵌套结构

enum Token {
    Native,
    ERC20 { address: String, decimals: u8 },
    ERC721 { address: String, token_id: u64 },
}

enum Transfer {
    Single { token: Token, amount: u128 },
    Batch { tokens: Vec<Token> },
}

fn describe_transfer(transfer: &Transfer) {
    match transfer {
        // 嵌套解构
        Transfer::Single {
            token: Token::Native,
            amount,
        } => {
            println!("原生代币转账: {} wei", amount);
        }
        Transfer::Single {
            token: Token::ERC20 { address, decimals },
            amount,
        } => {
            let human_amount = *amount as f64 / 10_f64.powi(*decimals as i32);
            println!("ERC20 转账: {} (合约: {})", human_amount, address);
        }
        Transfer::Single {
            token: Token::ERC721 { address, token_id },
            ..
        } => {
            println!("NFT 转账: 合约 {} Token #{}", address, token_id);
        }
        Transfer::Batch { tokens } => {
            println!("批量转账: {} 个代币", tokens.len());
        }
    }
}

3. if let 和 let else:简洁的单分支匹配

当你只关心某一个变体时,用 match 显得过于冗长。if let 提供了更简洁的语法。

fn process_if_confirmed(status: &TransactionStatus) {
    // 只处理 Confirmed 情况
    if let TransactionStatus::Confirmed(block) = status {
        println!("在区块 {} 确认,开始后续处理", block);
        // 这里可以触发事件、更新数据库等
    }
    // 其他情况直接跳过,不需要写 _ => {}
}

// let-else:匹配失败时提前返回(Rust 1.65+)
fn get_block_number(status: &TransactionStatus) -> Option<u64> {
    let TransactionStatus::Confirmed(block) = status else {
        // 不是 Confirmed 状态,提前返回 None
        return None;
    };
    // 这里 block 已经被绑定,可以直接使用
    Some(*block)
}

4. Option<T>:空值的安全表达

Rust 没有 null。所有 "可能不存在" 的值都用 Option<T> 表达。编译器强制你处理 None 情况。

// Option 的定义(标准库已定义,这里展示原理)
// enum Option<T> {
//     Some(T),
//     None,
// }

use std::collections::HashMap;

struct TokenRegistry {
    tokens: HashMap<String, TokenInfo>,
}

struct TokenInfo {
    name: String,
    symbol: String,
    decimals: u8,
    total_supply: u128,
}

impl TokenRegistry {
    fn new() -> Self {
        TokenRegistry {
            tokens: HashMap::new(),
        }
    }

    // 返回 Option:可能找到,也可能找不到
    fn get_token(&self, address: &str) -> Option<&TokenInfo> {
        self.tokens.get(address)
    }

    // 使用 Option 的各种方法
    fn get_symbol(&self, address: &str) -> String {
        // 方法1:match
        match self.get_token(address) {
            Some(token) => token.symbol.clone(),
            None => "UNKNOWN".to_string(),
        }
    }

    fn get_symbol_v2(&self, address: &str) -> String {
        // 方法2:unwrap_or_else(推荐)
        self.get_token(address)
            .map(|t| t.symbol.clone())
            .unwrap_or_else(|| "UNKNOWN".to_string())
    }

    fn get_decimals(&self, address: &str) -> u8 {
        // 方法3:map + unwrap_or(适合简单默认值)
        self.get_token(address)
            .map(|t| t.decimals)
            .unwrap_or(18)  // 默认 18 位精度
    }

    // Option 链式调用
    fn get_formatted_supply(&self, address: &str) -> Option<String> {
        self.get_token(address)
            .map(|t| {
                let supply = t.total_supply as f64 / 10_f64.powi(t.decimals as i32);
                format!("{} {}", supply, t.symbol)
            })
    }
}

Option 的常用方法速查

fn option_methods_demo() {
    let some_value: Option<u64> = Some(42);
    let none_value: Option<u64> = None;

    // unwrap:有值返回值,无值 panic(仅测试时使用)
    let v = some_value.unwrap();  // 42
    // let v = none_value.unwrap();  // PANIC!

    // expect:同 unwrap,但可自定义 panic 消息
    let v = some_value.expect("区块号不应为空");

    // unwrap_or:无值时返回默认值
    let v = none_value.unwrap_or(0);  // 0

    // unwrap_or_default:使用类型的 Default trait
    let v: u64 = none_value.unwrap_or_default();  // 0

    // map:对 Some 中的值做变换
    let doubled = some_value.map(|x| x * 2);  // Some(84)

    // and_then(flatmap):链接可能返回 None 的操作
    let result = some_value.and_then(|x| {
        if x > 10 { Some(x - 10) } else { None }
    });  // Some(32)

    // filter:条件过滤
    let filtered = some_value.filter(|&x| x > 100);  // None

    // is_some / is_none:布尔检查
    assert!(some_value.is_some());
    assert!(none_value.is_none());

    // or:如果自身是 None,返回替代 Option
    let fallback = none_value.or(Some(999));  // Some(999)

    // zip:合并两个 Option
    let a = Some(10u64);
    let b = Some("ETH");
    let combined = a.zip(b);  // Some((10, "ETH"))
}

5. Result<T, E>:错误处理的正确姿势

Result 是 Rust 处理可恢复错误的核心类型。在区块链客户端开发中,几乎每个外部调用都可能失败。

// Result 的定义
// enum Result<T, E> {
//     Ok(T),
//     Err(E),
// }

#[derive(Debug)]
enum BlockchainError {
    NetworkError(String),
    InvalidAddress(String),
    InsufficientBalance { required: u128, available: u128 },
    GasTooLow { min_gas: u64, provided: u64 },
    ContractRevert(String),
    Timeout,
}

struct Wallet {
    address: String,
    balance: u128,
    nonce: u64,
}

impl Wallet {
    // 返回 Result:明确告知调用者可能的错误
    fn send_transaction(
        &mut self,
        to: &str,
        amount: u128,
        gas_limit: u64,
    ) -> Result<String, BlockchainError> {
        // 验证地址
        if !to.starts_with("0x") || to.len() != 42 {
            return Err(BlockchainError::InvalidAddress(
                format!("无效地址: {}", to),
            ));
        }

        // 检查余额
        let total_cost = amount + (gas_limit as u128 * 20); // 假设 gas price = 20
        if self.balance < total_cost {
            return Err(BlockchainError::InsufficientBalance {
                required: total_cost,
                available: self.balance,
            });
        }

        // 检查 Gas
        if gas_limit < 21000 {
            return Err(BlockchainError::GasTooLow {
                min_gas: 21000,
                provided: gas_limit,
            });
        }

        // 模拟发送成功
        self.balance -= total_cost;
        self.nonce += 1;

        Ok(format!("0x{:064x}", self.nonce))  // 模拟返回 tx hash
    }
}

6. ? 运算符:优雅的错误传播

? 运算符是 Rust 的语法糖:如果 ResultOk,取出值继续执行;如果是 Err立即从当前函数返回该错误

fn transfer_with_check(
    wallet: &mut Wallet,
    to: &str,
    amount: u128,
) -> Result<String, BlockchainError> {
    // 每个 ? 都可能提前返回错误
    // 无需手动写 match + return Err
    let gas_estimate = estimate_gas(to, amount)?;
    let tx_hash = wallet.send_transaction(to, amount, gas_estimate)?;
    let receipt = wait_for_confirmation(&tx_hash)?;

    Ok(format!("转账成功: {}, 确认区块: {}", tx_hash, receipt))
}

fn estimate_gas(to: &str, amount: u128) -> Result<u64, BlockchainError> {
    if amount == 0 {
        return Err(BlockchainError::ContractRevert(
            "金额不能为 0".to_string(),
        ));
    }
    Ok(21000 + if to.starts_with("0x00") { 10000 } else { 0 })
}

fn wait_for_confirmation(tx_hash: &str) -> Result<u64, BlockchainError> {
    // 模拟等待确认
    if tx_hash.is_empty() {
        return Err(BlockchainError::Timeout);
    }
    Ok(18_000_000) // 模拟区块号
}

代码实战:完整的交易状态机

use std::collections::HashMap;
use std::time::{SystemTime, UNIX_EPOCH};

// ========== 完整的交易状态枚举 ==========
#[derive(Debug, Clone)]
enum TxState {
    /// 交易已创建,尚未广播
    Created {
        created_at: u64,
    },
    /// 已广播到 mempool
    Broadcasted {
        created_at: u64,
        broadcast_at: u64,
        nonce: u64,
    },
    /// 已被矿工打包
    Mined {
        block_number: u64,
        block_hash: String,
        gas_used: u64,
        success: bool,
    },
    /// 交易最终确认(足够的区块确认数)
    Finalized {
        block_number: u64,
        confirmations: u64,
    },
    /// 交易被丢弃(超时、被替换等)
    Dropped {
        reason: DropReason,
    },
}

#[derive(Debug, Clone)]
enum DropReason {
    Timeout,
    Replaced { new_tx_hash: String },
    NonceTooLow,
    InsufficientFunds,
}

// ========== 交易追踪器 ==========
struct TransactionTracker {
    transactions: HashMap<String, TxState>,
}

impl TransactionTracker {
    fn new() -> Self {
        TransactionTracker {
            transactions: HashMap::new(),
        }
    }

    fn create_tx(&mut self, tx_hash: String) {
        let now = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .unwrap()
            .as_secs();
        self.transactions.insert(tx_hash, TxState::Created { created_at: now });
    }

    /// 推进交易状态——只允许合法的状态转换
    fn advance_state(
        &mut self,
        tx_hash: &str,
        new_state: TxState,
    ) -> Result<(), String> {
        let current = self.transactions.get(tx_hash)
            .ok_or_else(|| format!("交易 {} 不存在", tx_hash))?;

        // 状态转换合法性检查
        let valid = match (current, &new_state) {
            (TxState::Created { .. }, TxState::Broadcasted { .. }) => true,
            (TxState::Created { .. }, TxState::Dropped { .. }) => true,
            (TxState::Broadcasted { .. }, TxState::Mined { .. }) => true,
            (TxState::Broadcasted { .. }, TxState::Dropped { .. }) => true,
            (TxState::Mined { success: true, .. }, TxState::Finalized { .. }) => true,
            (TxState::Mined { success: false, .. }, TxState::Dropped { .. }) => true,
            _ => false,
        };

        if !valid {
            return Err(format!(
                "非法状态转换: {:?} -> {:?}",
                current, new_state
            ));
        }

        self.transactions.insert(tx_hash.to_string(), new_state);
        Ok(())
    }

    /// 获取交易的用户可读状态
    fn get_status_message(&self, tx_hash: &str) -> String {
        match self.transactions.get(tx_hash) {
            None => "交易不存在".to_string(),
            Some(state) => match state {
                TxState::Created { created_at } => {
                    format!("已创建({}),等待广播", created_at)
                }
                TxState::Broadcasted { nonce, .. } => {
                    format!("已广播(nonce: {}),等待打包", nonce)
                }
                TxState::Mined { block_number, gas_used, success, .. } => {
                    if *success {
                        format!("已上链(区块 #{},Gas: {}),等待确认", block_number, gas_used)
                    } else {
                        format!("执行失败(区块 #{},Gas: {})", block_number, gas_used)
                    }
                }
                TxState::Finalized { block_number, confirmations } => {
                    format!("已确认(区块 #{},{} 个确认)", block_number, confirmations)
                }
                TxState::Dropped { reason } => {
                    match reason {
                        DropReason::Timeout => "已超时丢弃".to_string(),
                        DropReason::Replaced { new_tx_hash } => {
                            format!("已被替换: {}", new_tx_hash)
                        }
                        DropReason::NonceTooLow => "Nonce 已过期".to_string(),
                        DropReason::InsufficientFunds => "余额不足".to_string(),
                    }
                }
            },
        }
    }

    /// 统计各状态的交易数量
    fn get_stats(&self) -> HashMap<&str, usize> {
        let mut stats = HashMap::new();
        for state in self.transactions.values() {
            let key = match state {
                TxState::Created { .. } => "created",
                TxState::Broadcasted { .. } => "broadcasted",
                TxState::Mined { .. } => "mined",
                TxState::Finalized { .. } => "finalized",
                TxState::Dropped { .. } => "dropped",
            };
            *stats.entry(key).or_insert(0) += 1;
        }
        stats
    }
}

fn main() {
    let mut tracker = TransactionTracker::new();

    // 创建交易
    tracker.create_tx("0xabc123".to_string());
    println!("{}", tracker.get_status_message("0xabc123"));

    // 推进到广播状态
    let result = tracker.advance_state("0xabc123", TxState::Broadcasted {
        created_at: 1000,
        broadcast_at: 1001,
        nonce: 42,
    });
    assert!(result.is_ok());
    println!("{}", tracker.get_status_message("0xabc123"));

    // 非法状态转换:从广播直接到 Finalized
    let result = tracker.advance_state("0xabc123", TxState::Finalized {
        block_number: 100,
        confirmations: 12,
    });
    assert!(result.is_err());
    println!("拒绝非法转换: {}", result.unwrap_err());

    // 正确路径:广播 -> 上链 -> 确认
    tracker.advance_state("0xabc123", TxState::Mined {
        block_number: 18_500_000,
        block_hash: "0xdef456".to_string(),
        gas_used: 21000,
        success: true,
    }).unwrap();

    tracker.advance_state("0xabc123", TxState::Finalized {
        block_number: 18_500_000,
        confirmations: 12,
    }).unwrap();

    println!("{}", tracker.get_status_message("0xabc123"));
    println!("统计: {:?}", tracker.get_stats());
}

关键要点总结

要点说明
Rust enum 是 ADT每个变体可携带不同类型数据,Solidity enum 只是整数标签
match 必须穷尽编译器强制处理所有分支,遗漏会报错
if let 处理单分支只关心一个变体时比 match 简洁
Option 替代 null编译期强制处理空值,杜绝空指针异常
Result 替代异常错误是返回值的一部分,必须显式处理
? 简化错误传播等价于 match + early return,链式调用更清晰
永远不要在生产代码中 unwrap?unwrap_ormatch 代替

常见误区

误区 1:在生产代码中大量使用 unwrap()

// 错误:任何 None 都会 panic
let balance = get_balance(address).unwrap();

// 正确:优雅处理
let balance = get_balance(address).unwrap_or(0);
// 或者
let balance = get_balance(address)
    .ok_or_else(|| BlockchainError::InvalidAddress("未知地址".into()))?;

误区 2:把 Option 和 Result 当作相同的东西

  • Option 表示 "有或没有",没有错误信息
  • Result 表示 "成功或失败",Err 携带错误详情
  • 两者可以互转:option.ok_or(err)result.ok()

误区 3:忽略 match 的穷尽性

// 编译错误!缺少 Dropped 分支
// match state {
//     TxState::Created { .. } => {},
//     TxState::Broadcasted { .. } => {},
//     TxState::Mined { .. } => {},
//     TxState::Finalized { .. } => {},
//     // 遗漏了 Dropped
// }

面试关联

Q: Rust 的 enum 和 Solidity 的 enum 有什么本质区别?

30 秒回答:Solidity 的 enum 只是 uint8 的语法糖,不能携带数据。Rust 的 enum 是代数数据类型,每个变体可以持有不同类型和数量的数据,配合 match 的穷尽性检查,可以在编译期保证所有状态都被正确处理。

追问:这对智能合约设计有什么影响?

  • Solidity 中需要 enum + 多个 mapping 来表达状态关联数据,容易出现状态与数据不一致
  • 如果 Solidity 有 Rust 式 enum,可以大幅减少存储槽使用和逻辑错误

Q: 为什么 Rust 选择 Option/Result 而不是异常机制?

  • 异常是隐式控制流,调用者可能忘记处理
  • Result 是显式返回值,编译器强制处理
  • 在区块链场景中,遗漏错误处理可能导致资金损失,显式错误处理更安全
  • ? 运算符让 Result 的使用体验接近异常,但保持了类型安全

参考资源

资源说明
The Rust Book - Ch6: Enums官方教程,必读
The Rust Book - Ch9: Error Handling错误处理完整指南
Rust by Example - Enums更多实例
Solidity Enum 文档对比理解
Error Handling in Rust (blog)Andrew Gallant 的经典文章