Rust - 闭包(closures) + 函数指针 + 生命周期深入('a标注)
### 1. 闭包(Closure):捕获环境的匿名函数
日期: 2026-04-18 方向: Rust 阶段: 第一阶段:基础构建 标签: #rust #closures #fn-traits #lifetime #function-pointer #move-semantics
今日目标
| 类型 | 内容 |
|---|---|
| 学习 | 掌握闭包语法与三种 Fn trait 的区别、函数指针类型、生命周期标注规则与省略规则 |
| 实操 | 实现交易过滤闭包系统、带生命周期的结构体引用链、完整的 blockchain client 生命周期示例 |
| 产出 | 闭包 trait 对比表 + 生命周期规则总结 + 完整代码 + 面试题 |
核心概念
1. 闭包(Closure):捕获环境的匿名函数
闭包是 Rust 中可以捕获其定义环境中变量的匿名函数。与普通函数不同,闭包可以"记住"它被创建时的上下文。这在 Web3 场景中极其常见——例如,你需要根据不同条件动态构建交易过滤器。
基础语法
fn main() {
// 最简闭包:类型由编译器推断
let add_one = |x| x + 1;
println!("{}", add_one(5)); // 6
// 显式标注类型
let add_two: fn(i32) -> i32 = |x: i32| -> i32 { x + 2 };
println!("{}", add_two(5)); // 7
// 多参数闭包
let multiply = |a: u64, b: u64| a * b;
println!("{}", multiply(3, 7)); // 21
// 多行闭包,用花括号包裹
let calculate_gas = |base_fee: u64, priority_fee: u64, gas_used: u64| {
let total_fee_per_gas = base_fee + priority_fee;
let total_cost = total_fee_per_gas * gas_used;
total_cost
};
// base_fee=30 gwei, priority=2 gwei, gas_used=21000
let cost = calculate_gas(30, 2, 21000);
println!("Gas cost: {} gwei", cost); // 672000 gwei
}
关键区别:闭包与普通函数的语法差异在于使用 |...| 而非 fn(...) 作为参数列表,且类型标注是可选的。编译器会根据闭包首次被调用时的实参类型来推断参数和返回值类型,一旦推断完成,类型就被固定。
闭包捕获环境
这是闭包区别于普通函数的核心特性。闭包可以从定义它的作用域中捕获变量。
fn main() {
let min_value_usd = 1000_u64; // 被闭包捕获(不可变借用)
let blocked_addresses = vec![ // 被闭包捕获(不可变借用)
"0xdead".to_string(),
"0xbad0".to_string(),
];
// 闭包捕获了外部变量 min_value_usd 和 blocked_addresses
let is_valid_tx = |sender: &str, value: u64| -> bool {
if value < min_value_usd {
return false;
}
if blocked_addresses.iter().any(|addr| addr == sender) {
return false;
}
true
};
println!("{}", is_valid_tx("0xabcd", 2000)); // true
println!("{}", is_valid_tx("0xdead", 5000)); // false(被封锁)
println!("{}", is_valid_tx("0xabcd", 500)); // false(金额不足)
// min_value_usd 和 blocked_addresses 仍然可用,因为闭包只是借用
println!("最低门槛: {} USD", min_value_usd);
println!("封锁地址数: {}", blocked_addresses.len());
}
2. 三种 Fn Trait:Fn / FnMut / FnOnce
Rust 编译器根据闭包对捕获变量的使用方式,自动为闭包实现不同的 trait。这三个 trait 形成了一个层次关系:
| Trait | 捕获方式 | 可调用次数 | 典型场景 |
|---|---|---|---|
Fn | 不可变借用 &T | 无限次 | 只读过滤、映射、查询 |
FnMut | 可变借用 &mut T | 无限次 | 累加器、统计、状态修改 |
FnOnce | 获取所有权 T | 恰好一次 | 消耗数据、线程传递、一次性初始化 |
层次关系:Fn 是 FnMut 的子 trait,FnMut 是 FnOnce 的子 trait。所以实现了 Fn 的闭包也可以在需要 FnMut 或 FnOnce 的地方使用。
/// 示例:Fn —— 不可变借用捕获的变量
fn demonstrate_fn() {
let chain_name = String::from("Ethereum");
// 闭包只读取 chain_name,所以实现了 Fn
let get_rpc_url = || {
format!("https://rpc.{}.org", chain_name.to_lowercase())
};
// 可以调用多次
println!("{}", get_rpc_url());
println!("{}", get_rpc_url());
// chain_name 仍然可用
println!("Chain: {}", chain_name);
}
/// 示例:FnMut —— 可变借用捕获的变量
fn demonstrate_fn_mut() {
let mut total_volume: u128 = 0;
let mut tx_count: u32 = 0;
// 闭包修改 total_volume 和 tx_count,所以需要 FnMut
let mut record_trade = |amount: u128| {
total_volume += amount;
tx_count += 1;
println!("Trade #{}: {} USD, cumulative: {} USD", tx_count, amount, total_volume);
};
record_trade(50000);
record_trade(120000);
record_trade(30000);
// 闭包结束使用后,可以再次访问变量
println!("总交易量: {}, 总笔数: {}", total_volume, tx_count);
}
/// 示例:FnOnce —— 获取捕获变量的所有权
fn demonstrate_fn_once() {
let private_key = String::from("0x1234...secret_key");
// 闭包将 private_key 移动(move)进来并消耗掉
let sign_and_destroy = || {
let signature = format!("signed_with_{}", private_key);
// private_key 的所有权被转移到闭包内部
drop(private_key); // 显式消耗
signature
};
let sig = sign_and_destroy();
println!("Signature: {}", sig);
// sign_and_destroy(); // 编译错误!FnOnce 只能调用一次
// println!("{}", private_key); // 编译错误!所有权已移动
}
在函数参数中使用 Fn trait
当你编写接受闭包作为参数的函数时,需要用 trait bound 指定闭包的类型。
use std::fmt;
#[derive(Debug, Clone)]
struct Transaction {
hash: String,
from: String,
to: String,
value_eth: f64,
gas_used: u64,
success: bool,
}
impl fmt::Display for Transaction {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}: {} -> {} ({:.4} ETH)",
&self.hash[..8], &self.from[..8], &self.to[..8], self.value_eth)
}
}
/// 使用 Fn trait bound:过滤器只需要不可变读取
fn filter_transactions<F>(txs: &[Transaction], predicate: F) -> Vec<&Transaction>
where
F: Fn(&Transaction) -> bool,
{
txs.iter().filter(|tx| predicate(tx)).collect()
}
/// 使用 FnMut trait bound:回调可能修改外部状态
fn process_transactions<F>(txs: &[Transaction], mut callback: F)
where
F: FnMut(&Transaction),
{
for tx in txs {
callback(tx);
}
}
/// 使用 FnOnce trait bound:工厂函数只需调用一次
fn create_and_execute<F, R>(factory: F) -> R
where
F: FnOnce() -> R,
{
factory()
}
fn main() {
let transactions = vec![
Transaction {
hash: "0xabcdef1234567890".into(),
from: "0x1111111111111111".into(),
to: "0x2222222222222222".into(),
value_eth: 1.5,
gas_used: 21000,
success: true,
},
Transaction {
hash: "0x9876543210fedcba".into(),
from: "0x3333333333333333".into(),
to: "0x4444444444444444".into(),
value_eth: 0.05,
gas_used: 65000,
success: false,
},
Transaction {
hash: "0xdeadbeefcafebabe".into(),
from: "0x1111111111111111".into(),
to: "0x5555555555555555".into(),
value_eth: 100.0,
gas_used: 21000,
success: true,
},
];
// --- Fn: 只读过滤 ---
// 大额交易过滤器
let whale_txs = filter_transactions(&transactions, |tx| tx.value_eth > 10.0);
println!("=== 大额交易 (>10 ETH) ===");
for tx in &whale_txs {
println!(" {}", tx);
}
// 失败交易过滤器
let failed_txs = filter_transactions(&transactions, |tx| !tx.success);
println!("\n=== 失败交易 ===");
for tx in &failed_txs {
println!(" {}", tx);
}
// --- FnMut: 累计统计 ---
let mut total_gas = 0u64;
let mut total_value = 0.0f64;
process_transactions(&transactions, |tx| {
total_gas += tx.gas_used;
total_value += tx.value_eth;
});
println!("\n总 Gas: {}, 总价值: {:.4} ETH", total_gas, total_value);
// --- FnOnce: 一次性工厂 ---
let config_data = vec!["mainnet".to_string(), "https://rpc.eth.org".to_string()];
let client = create_and_execute(|| {
// config_data 的所有权被移入闭包
format!("Client(network={}, rpc={})", config_data[0], config_data[1])
});
println!("\n{}", client);
// config_data 已被消耗,无法再使用
}
3. move 闭包
move 关键字强制闭包获取所有捕获变量的所有权,而不是借用。这在多线程和异步编程中至关重要——当闭包需要发送到另一个线程时,必须拥有它使用的数据。
use std::thread;
use std::sync::mpsc;
fn main() {
let (tx, rx) = mpsc::channel();
let addresses = vec![
"0xUniswapRouter".to_string(),
"0xAavePool".to_string(),
"0xCompoundComet".to_string(),
];
// 没有 move —— 编译错误:
// addresses 可能在线程运行时被 drop
// let handle = thread::spawn(|| { ... addresses ... });
// 使用 move —— 所有权转移给线程
let handle = thread::spawn(move || {
for addr in &addresses {
println!("[Worker] 扫描合约: {}", addr);
tx.send(format!("scanned:{}", addr)).unwrap();
}
// addresses 在线程结束时被 drop
});
// 主线程接收结果
for received in rx {
println!("[Main] 收到: {}", received);
}
handle.join().unwrap();
// println!("{:?}", addresses); // 编译错误!所有权已移走
}
move + Copy 类型的特殊情况:对于实现了 Copy trait 的类型(如 i32、bool、f64),move 会复制一份进闭包,原变量仍然可用。
fn main() {
let threshold = 100u64; // u64 实现了 Copy
let label = String::from("gas"); // String 没有 Copy
let check = move || {
println!("{}: {}", label, threshold);
};
check();
println!("threshold 仍可用: {}", threshold); // OK,u64 被复制
// println!("{}", label); // 编译错误!String 被移走
}
4. 函数指针 fn
函数指针 fn (小写) 是一个具体的类型,而 Fn/FnMut/FnOnce (大写) 是 trait。函数指针不捕获环境,所以它总是满足所有三个 Fn trait。
// 函数指针类型:fn(参数类型) -> 返回类型
type GasEstimator = fn(u64, u64) -> u64;
type AddressValidator = fn(&str) -> bool;
fn estimate_eip1559_gas(base_fee: u64, priority_fee: u64) -> u64 {
base_fee + priority_fee
}
fn estimate_legacy_gas(gas_price: u64, _unused: u64) -> u64 {
gas_price
}
fn is_valid_eth_address(addr: &str) -> bool {
addr.starts_with("0x") && addr.len() == 42
}
/// 接受函数指针作为参数
fn calculate_tx_cost(estimator: GasEstimator, param1: u64, param2: u64, gas_limit: u64) -> u64 {
estimator(param1, param2) * gas_limit
}
fn main() {
// 函数指针可以像普通值一样传递
let estimator: GasEstimator = estimate_eip1559_gas;
let cost = calculate_tx_cost(estimator, 30, 2, 21000);
println!("EIP-1559 cost: {} gwei", cost);
let cost_legacy = calculate_tx_cost(estimate_legacy_gas, 35, 0, 21000);
println!("Legacy cost: {} gwei", cost_legacy);
// 函数指针 vs 闭包的区别
let validators: Vec<fn(&str) -> bool> = vec![
is_valid_eth_address,
|addr| addr.len() > 0, // 不捕获环境的闭包可以强转为 fn
];
let test_addr = "0x1234567890abcdef1234567890abcdef12345678";
for (i, validator) in validators.iter().enumerate() {
println!("Validator {}: {}", i, validator(test_addr));
}
}
何时用函数指针 vs Fn trait:
- 用
fn指针:当你确定不需要捕获环境,或需要存储在数组/结构体中 - 用
Fntrait bound:当你需要接受闭包(可能捕获环境),更灵活
5. 生命周期(Lifetime)深入
生命周期是 Rust 最独特的概念之一,它确保所有引用在使用期间一定是有效的。大多数时候编译器可以自动推断生命周期,但某些场景必须手动标注。
为什么需要生命周期标注?
// 编译错误:返回引用但编译器不知道它的生命周期
// fn longest(x: &str, y: &str) -> &str {
// if x.len() > y.len() { x } else { y }
// }
// 正确:用 'a 告诉编译器"返回值至少与 x 和 y 中较短的那个活得一样久"
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
fn main() {
let chain1 = String::from("Ethereum Mainnet");
let result;
{
let chain2 = String::from("Polygon");
result = longest(&chain1, &chain2);
println!("Longer: {}", result); // OK,两个引用都有效
}
// println!("{}", result);
// 如果取消注释,编译错误!chain2 已被 drop,result 可能指向无效数据
}
核心规则:生命周期标注不改变任何引用的实际存活时间,它只是帮助编译器验证引用关系的正确性。
生命周期省略规则(Lifetime Elision Rules)
编译器自动推断生命周期的三条规则:
// 规则 1: 每个引用参数获得独立的生命周期
// fn foo(x: &str) 自动变成 fn foo<'a>(x: &'a str)
// fn foo(x: &str, y: &str) 变成 fn foo<'a, 'b>(x: &'a str, y: &'b str)
// 规则 2: 如果只有一个输入生命周期,它自动赋给所有输出引用
// fn first_word(s: &str) -> &str 变成 fn first_word<'a>(s: &'a str) -> &'a str
fn first_word(s: &str) -> &str {
let bytes = s.as_bytes();
for (i, &byte) in bytes.iter().enumerate() {
if byte == b' ' {
return &s[..i];
}
}
s
}
// 规则 3: 如果是方法(第一个参数是 &self 或 &mut self),
// self 的生命周期赋给所有输出引用
struct Wallet {
address: String,
label: String,
}
impl Wallet {
// 自动推断为 fn get_label<'a>(&'a self) -> &'a str
fn get_label(&self) -> &str {
&self.label
}
}
// 三条规则都无法确定时 —— 必须手动标注
// fn longest(x: &str, y: &str) -> &str 有两个输入引用,不符合规则 2
// 不是方法,不符合规则 3
// 所以必须写 fn longest<'a>(x: &'a str, y: &'a str) -> &'a str
结构体中的生命周期
当结构体持有引用时,必须标注生命周期,以确保结构体不会比它引用的数据活得更久。
/// 交易收据的"视图"——不拥有数据,只引用原始数据
/// 'a 表示:TxView 中所有引用的数据至少活到 'a
#[derive(Debug)]
struct TxView<'a> {
hash: &'a str,
from: &'a str,
to: &'a str,
block_number: u64, // 值类型不需要生命周期
}
impl<'a> TxView<'a> {
fn new(hash: &'a str, from: &'a str, to: &'a str, block_number: u64) -> Self {
TxView { hash, from, to, block_number }
}
// 返回引用绑定到 self 的生命周期(规则 3)
fn summary(&self) -> String {
format!("#{} {} -> {} (block {})",
&self.hash[..8], &self.from[..8], &self.to[..8], self.block_number)
}
// 返回引用,手动标注为 'a(与结构体字段同生命周期)
fn sender(&self) -> &'a str {
self.from
}
}
/// 区块视图——引用一组交易视图
#[derive(Debug)]
struct BlockView<'a> {
number: u64,
transactions: Vec<TxView<'a>>,
}
impl<'a> BlockView<'a> {
/// 过滤特定发送者的交易
fn txs_from(&self, sender: &str) -> Vec<&TxView<'a>> {
self.transactions.iter().filter(|tx| tx.from == sender).collect()
}
/// 获取最大区块号
fn latest_block_number(&self) -> u64 {
self.number
}
}
fn main() {
let hash1 = String::from("0xabc123def456abc123def456abc123def456abc123def456abc123def456abc1");
let hash2 = String::from("0x999888777666555444333222111000aaabbbcccdddeeefff000111222333444");
let alice = String::from("0xAliceAddress000000000000000000000000000001");
let bob = String::from("0xBobAddress00000000000000000000000000000002");
let carol = String::from("0xCarolAddress000000000000000000000000000003");
let block = BlockView {
number: 19_000_000,
transactions: vec![
TxView::new(&hash1, &alice, &bob, 19_000_000),
TxView::new(&hash2, &bob, &carol, 19_000_000),
],
};
println!("Block #{}", block.latest_block_number());
for tx in &block.transactions {
println!(" {}", tx.summary());
}
let alice_txs = block.txs_from(&alice);
println!("\nAlice 的交易: {} 笔", alice_txs.len());
// 关键:block, hash1, alice 等必须比 BlockView 活得更久
// 如果任何一个被 drop,BlockView 中的引用就会悬空
}
多个生命周期参数
当函数中的不同引用有不同的生存范围时,需要多个生命周期参数。
/// 'config 和 'data 是两个独立的生命周期
/// 配置通常比数据活得更久
struct RpcClient<'config, 'data> {
endpoint: &'config str,
api_key: &'config str,
last_response: Option<&'data str>,
}
impl<'config, 'data> RpcClient<'config, 'data> {
fn new(endpoint: &'config str, api_key: &'config str) -> Self {
RpcClient {
endpoint,
api_key,
last_response: None,
}
}
fn set_response(&mut self, response: &'data str) {
self.last_response = Some(response);
}
fn endpoint(&self) -> &'config str {
self.endpoint
}
}
fn main() {
let endpoint = "https://eth-mainnet.alchemyapi.io/v2"; // 'static
let api_key = "demo-key-123"; // 'static
let mut client = RpcClient::new(endpoint, api_key);
{
let response_data = String::from(r#"{"jsonrpc":"2.0","result":"0x1"}"#);
client.set_response(&response_data);
println!("Response: {:?}", client.last_response);
// response_data 将在此块结束时被 drop
}
// client.last_response 现在引用的数据已无效
// 但 endpoint 和 api_key 仍然有效(它们是 'static)
println!("Endpoint still valid: {}", client.endpoint());
}
'static 生命周期
'static 表示引用在整个程序运行期间都有效。字符串字面量就是 'static 的。
// 字符串字面量是 &'static str
let chain: &'static str = "Ethereum";
// 泛型约束中的 'static 表示"类型不包含任何非 static 引用"
// 注意:拥有所有权的类型(如 String)满足 'static,因为它不依赖任何引用
fn spawn_scanner<F>(task: F)
where
F: FnOnce() + Send + 'static,
{
std::thread::spawn(task);
}
fn main() {
let owned_address = String::from("0xSomeAddress"); // 拥有所有权
// OK: move 闭包获取 owned_address 的所有权,满足 'static
spawn_scanner(move || {
println!("Scanning: {}", owned_address);
});
// 不 OK(如果不用 move):
// let borrowed = &String::from("0xAddr");
// spawn_scanner(|| { println!("{}", borrowed); }); // 编译错误
std::thread::sleep(std::time::Duration::from_millis(100));
}
代码实战:完整的交易过滤系统
将闭包和生命周期结合起来,构建一个实用的交易过滤和分析系统。
use std::collections::HashMap;
// ========== 数据定义 ==========
#[derive(Debug, Clone)]
struct Transaction {
hash: String,
from: String,
to: String,
value_wei: u128,
gas_price: u64,
gas_used: u64,
success: bool,
method_id: String, // 函数签名的前 4 字节
}
impl Transaction {
fn value_eth(&self) -> f64 {
self.value_wei as f64 / 1e18
}
fn gas_cost_gwei(&self) -> u64 {
self.gas_price * self.gas_used
}
}
// ========== 过滤器构建器(利用闭包) ==========
/// TransactionFilter 存储一组闭包,每个闭包代表一个过滤条件
/// 使用 Box<dyn Fn> 允许存储不同类型的闭包
struct TransactionFilter {
name: String,
predicates: Vec<Box<dyn Fn(&Transaction) -> bool>>,
}
impl TransactionFilter {
fn new(name: &str) -> Self {
TransactionFilter {
name: name.to_string(),
predicates: Vec::new(),
}
}
/// 链式 API: 添加最小金额过滤
fn min_value_eth(mut self, min: f64) -> Self {
self.predicates.push(Box::new(move |tx| tx.value_eth() >= min));
self
}
/// 链式 API: 只要成功的交易
fn successful_only(mut self) -> Self {
self.predicates.push(Box::new(|tx| tx.success));
self
}
/// 链式 API: 排除特定地址
fn exclude_addresses(mut self, addresses: Vec<String>) -> Self {
// addresses 被 move 进闭包
self.predicates.push(Box::new(move |tx| {
!addresses.contains(&tx.from) && !addresses.contains(&tx.to)
}));
self
}
/// 链式 API: 特定方法调用
fn method_id(mut self, method: String) -> Self {
self.predicates.push(Box::new(move |tx| tx.method_id == method));
self
}
/// 链式 API: 自定义条件
fn custom<F>(mut self, predicate: F) -> Self
where
F: Fn(&Transaction) -> bool + 'static,
{
self.predicates.push(Box::new(predicate));
self
}
/// 应用所有过滤器
fn apply<'a>(&self, txs: &'a [Transaction]) -> Vec<&'a Transaction> {
txs.iter()
.filter(|tx| self.predicates.iter().all(|p| p(tx)))
.collect()
}
}
// ========== 分析器(使用 FnMut 闭包) ==========
struct TxAnalyzer;
impl TxAnalyzer {
/// 按分组键聚合交易,callback 用来提取分组键
fn group_by<'a, F>(txs: &'a [Transaction], key_fn: F) -> HashMap<String, Vec<&'a Transaction>>
where
F: Fn(&Transaction) -> String,
{
let mut groups: HashMap<String, Vec<&Transaction>> = HashMap::new();
for tx in txs {
let key = key_fn(tx);
groups.entry(key).or_default().push(tx);
}
groups
}
/// 用 FnMut 回调做带状态的聚合
fn aggregate<F, R>(txs: &[Transaction], init: R, mut folder: F) -> R
where
F: FnMut(R, &Transaction) -> R,
{
let mut acc = init;
for tx in txs {
acc = folder(acc, tx);
}
acc
}
}
// ========== 结果视图(使用生命周期) ==========
#[derive(Debug)]
struct FilterResult<'a> {
filter_name: &'a str,
matched: Vec<&'a Transaction>,
total_scanned: usize,
}
impl<'a> FilterResult<'a> {
fn match_rate(&self) -> f64 {
if self.total_scanned == 0 {
return 0.0;
}
self.matched.len() as f64 / self.total_scanned as f64 * 100.0
}
fn total_value_eth(&self) -> f64 {
self.matched.iter().map(|tx| tx.value_eth()).sum()
}
fn summary(&self) -> String {
format!(
"[{}] {}/{} matched ({:.1}%), total value: {:.4} ETH",
self.filter_name,
self.matched.len(),
self.total_scanned,
self.match_rate(),
self.total_value_eth(),
)
}
}
fn run_filter<'a>(filter: &'a TransactionFilter, txs: &'a [Transaction]) -> FilterResult<'a> {
let matched = filter.apply(txs);
FilterResult {
filter_name: &filter.name,
matched,
total_scanned: txs.len(),
}
}
fn main() {
// 模拟交易数据
let transactions = vec![
Transaction {
hash: "0xaaa111".into(), from: "0xAlice".into(), to: "0xUniswap".into(),
value_wei: 5_000_000_000_000_000_000, gas_price: 30, gas_used: 150000,
success: true, method_id: "0x38ed1739".into(), // swapExactTokensForTokens
},
Transaction {
hash: "0xbbb222".into(), from: "0xBob".into(), to: "0xAave".into(),
value_wei: 100_000_000_000_000_000_000, gas_price: 25, gas_used: 250000,
success: true, method_id: "0xe8eda9df".into(), // deposit
},
Transaction {
hash: "0xccc333".into(), from: "0xMEVBot".into(), to: "0xUniswap".into(),
value_wei: 50_000_000_000_000_000_000, gas_price: 200, gas_used: 300000,
success: true, method_id: "0x38ed1739".into(),
},
Transaction {
hash: "0xddd444".into(), from: "0xAlice".into(), to: "0xCompound".into(),
value_wei: 1_000_000_000_000_000_000, gas_price: 28, gas_used: 180000,
success: false, method_id: "0xa0712d68".into(), // mint
},
Transaction {
hash: "0xeee555".into(), from: "0xWhale".into(), to: "0xUniswap".into(),
value_wei: 500_000_000_000_000_000_000, gas_price: 35, gas_used: 200000,
success: true, method_id: "0x38ed1739".into(),
},
];
// --- 构建过滤器 ---
let whale_filter = TransactionFilter::new("大额交易")
.min_value_eth(10.0)
.successful_only();
let clean_swap_filter = TransactionFilter::new("正常Swap")
.method_id("0x38ed1739".into())
.successful_only()
.exclude_addresses(vec!["0xMEVBot".into()])
.custom(|tx| tx.gas_price < 100); // 排除高 gas(可能是 MEV)
// --- 执行过滤 ---
let whale_result = run_filter(&whale_filter, &transactions);
let swap_result = run_filter(&clean_swap_filter, &transactions);
println!("{}", whale_result.summary());
for tx in &whale_result.matched {
println!(" {} ({:.2} ETH)", tx.hash, tx.value_eth());
}
println!("\n{}", swap_result.summary());
for tx in &swap_result.matched {
println!(" {} from {}", tx.hash, tx.from);
}
// --- 聚合分析 ---
let by_sender = TxAnalyzer::group_by(&transactions, |tx| tx.from.clone());
println!("\n=== 按发送者分组 ===");
for (sender, txs) in &by_sender {
let vol: f64 = txs.iter().map(|tx| tx.value_eth()).sum();
println!(" {}: {} 笔, {:.2} ETH", sender, txs.len(), vol);
}
// FnMut 聚合:计算成功交易的总 gas 消耗
let total_gas = TxAnalyzer::aggregate(
&transactions,
0u64,
|acc, tx| {
if tx.success { acc + tx.gas_cost_gwei() } else { acc }
},
);
println!("\n成功交易总 Gas: {} gwei", total_gas);
}
关键要点总结
闭包要点
| 要点 | 说明 |
|---|---|
| 闭包自动推断类型 | 首次调用时确定,之后不可变更 |
| 三种 Fn trait 自动实现 | 编译器根据闭包体对捕获变量的使用方式决定 |
Fn 最严格但最灵活 | 只读借用,可多次调用,可在要求 FnMut 或 FnOnce 的地方使用 |
move 转移所有权 | 多线程和异步场景必备,Copy 类型会被复制 |
Box<dyn Fn> 存储闭包 | 闭包类型是匿名的,用 trait object 可以存储在集合中 |
生命周期要点
| 要点 | 说明 |
|---|---|
| 标注不改变实际生命期 | 只是告诉编译器引用之间的关系 |
| 三条省略规则能覆盖多数场景 | 函数参数和返回值的引用通常不需要手动标注 |
| 结构体持有引用必须标注 | struct Foo<'a> { field: &'a str } |
'static 表示整个程序生命期 | 字符串字面量和拥有所有权的类型满足 'static |
| 多个生命周期参数表示不同的约束 | 当不同引用有不同生存范围时使用 |
常见误区
误区 1:以为闭包总是堆分配
// 错误认知:闭包一定要 Box::new()
// 事实:只有当需要类型擦除(存入集合、作为返回值)时才需要 Box
// 栈上闭包 —— 零开销
fn apply_filter<F: Fn(u64) -> bool>(value: u64, f: F) -> bool {
f(value)
}
// 堆上闭包 —— 用于需要统一类型的场景
let filters: Vec<Box<dyn Fn(u64) -> bool>> = vec![
Box::new(|x| x > 100),
Box::new(|x| x % 2 == 0),
];
误区 2:到处加 'static
// 新手常犯:看到编译错误就加 'static
// struct MyStruct<'static> { data: &'static str } // 过度约束
// 正确做法:只在真正需要时用 'static
struct Config {
// 如果数据来自配置文件,用 String 拥有所有权
rpc_url: String,
}
struct ConfigView<'a> {
// 如果只是临时引用配置,用生命周期参数
rpc_url: &'a str,
}
误区 3:混淆 fn 和 Fn
// fn(小写)—— 函数指针类型,不能捕获环境
let fp: fn(i32) -> i32 = |x| x + 1; // OK,没有捕获
let y = 10;
// let fp2: fn(i32) -> i32 = |x| x + y; // 编译错误!捕获了 y
// Fn(大写)—— trait,可以捕获环境
let closure: Box<dyn Fn(i32) -> i32> = Box::new(|x| x + y); // OK
误区 4:以为生命周期标注使引用活更久
// 生命周期标注不延长任何变量的生命期
fn wrong_thinking<'a>() -> &'a str {
let local = String::from("hello");
// &local // 编译错误!local 在函数结束就被 drop
// // 'a 不能让 local 活得更久
"hello" // 这可以,因为字符串字面量是 'static
}
面试关联
Q1: Rust 闭包和 C++/JavaScript 闭包有什么区别?
核心答案:Rust 闭包在编译期通过类型系统精确追踪闭包如何使用捕获的变量(通过 Fn/FnMut/FnOnce trait),这保证了内存安全且无 GC 开销。C++ lambda 有类似的值捕获/引用捕获区分但不强制安全性,JavaScript 闭包始终通过 GC 引用捕获。
展开:
- Rust 的三种 trait 让编译器在编译期就能判断闭包的行为——只读、可修改、消耗性
- 闭包在 Rust 中是零成本抽象,不捕获环境时和函数指针完全等价
- 在区块链场景中,这种精确性意味着你可以放心将闭包传入多线程(用
move + Send + 'static),而不担心数据竞争
Q2: 什么时候必须手动标注生命周期?
核心答案:当编译器的三条省略规则无法确定返回值引用的生命周期时,必须手动标注。最常见的场景是函数有多个引用参数且返回引用。
展开:
- 规则 1:每个引用参数获得独立生命周期
- 规则 2:单引用参数时,其生命周期赋给返回值
- 规则 3:方法中
&self的生命周期赋给返回值 - 结构体持有引用时总是需要标注
- 实际项目中更常见的做法是使用
String(拥有所有权)而非&str(引用),除非有明确的性能需求
Q3: 在 Web3/区块链 Rust 项目中,闭包的典型用法?
答案:
- 交易过滤:
filter(|tx| tx.value > threshold)—— 动态构建过滤条件 - 异步回调:Tokio 异步运行时中,
move || async { ... }处理 RPC 响应 - 策略模式:将不同的 gas 估算策略、签名策略作为闭包传入
- Iterator 链:
txs.iter().filter(...).map(...).collect()构建数据管道
参考资源
| 资源 | 说明 |
|---|---|
| The Rust Book - Closures | 官方教程,闭包章节 |
| The Rust Book - Lifetimes | 官方教程,生命周期章节 |
| Rust by Example - Closures | 大量示例代码 |
| Rust Nomicon - Lifetimes | 高级生命周期概念 |
| Common Rust Lifetime Misconceptions | 常见误解纠正 |
| Jon Gjengset - Crust of Rust: Lifetime Annotations | 深入讲解视频 |