Rust - enum + 模式匹配(match) + Option/Result
### 1. Rust enum:不只是枚举,而是代数数据类型
日期: 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 的语法糖:如果 Result 是 Ok,取出值继续执行;如果是 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_or、match 代替 |
常见误区
误区 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 的经典文章 |