AI 辅助学习 Rust 宏:先让模型解释声明宏的匹配规则,再手写一个

📅 2026/7/9 16:58:24
AI 辅助学习 Rust 宏:先让模型解释声明宏的匹配规则,再手写一个
AI 辅助学习 Rust 宏先让模型解释声明宏的匹配规则再手写一个一、为什么 Rust 宏难学——从外到内的逆向理解Rust 宏本质上是写代码的代码。你先看到宏的调用方let v vec![1, 2, 3];然后你去看宏的定义#[macro_export] macro_rules! vec { ( $( $x:expr ),* ) { // ... }; }问题来了$($x:expr),*这行东西是什么意思它和1, 2, 3是怎么对应上的flowchart LR A[宏调用: vec! {1, 2, 3}] -- B{宏匹配器} B -- C[匹配模式: $ $x:expr ,*] C --|$x 捕获 1| D1[片段 1: expr] C --|$x 捕获 2| D2[片段 2: expr] C --|$x 捕获 3| D3[片段 3: expr] D1 -- E[展开为代码] D2 -- E D3 -- E B --|不匹配| F[编译错误] style B fill:#ffd93d,stroke:#333 style E fill:#6bcb77,stroke:#333 style F fill:#ff6b6b,stroke:#333传统的学习方式是读文档 → 看不懂 → 找教程 → 半懂 → 自己试。而 AI 辅助学习的优势是可以让你像老师带学生一样逐行让 AI 解释然后用自己的话复述一遍。二、让 AI 拆解声明宏从 surface syntax 到深层含义以下是我让 AI 解释一个常见宏的对话过程整理后案例println!宏的拆解// 标准库 println! 的简化版实现 macro_rules! my_println { // 分支 1: 无格式化参数 ($fmt:expr) { println!({}, $fmt) }; // 分支 2: 带格式化参数 ($fmt:expr, $($arg:tt)*) { println!($fmt, $($arg)*) }; }让 AI 解释这 6 行代码时AI 给出了以下拆解我整理后用费曼方式复述macro_rules!是创建声明宏的关键字——它和fn类似但定义的不是函数而是代码生成规则。($fmt:expr) { ... }是一个匹配分支arm()是匹配的定界符$fmt:expr表示匹配一个表达式捕获它命名为$fmt是如果匹配成功就展开为$($arg:tt)*是重复模式$()开始一个重复组$arg:tt匹配一个 token tree任意单个语法单元*表示重复零次或多次注意两个分支里有逗号的区别($fmt:expr, $($arg:tt)*)中的逗号是字面量展开体中$($arg)*表示把捕获的所有$arg在这里重复展开。AI 这样解释后我终于明白了宏就是模式匹配 代码生成。匹配规则用特殊语法描述接受的输入形状展开规则用$变量名引用捕获的内容。三、手写一个count_expr!宏理解了基本原理后我手写了一个练习宏计算传入表达式的数量。// count_expr! 宏 —— 计算传入表达式的数量 /// 统计传入宏的表达式的个数 /// /// # 用法 /// /// let n count_expr!(a, b, c); // n 3 /// let n count_expr!(1); // n 1 /// let n count_expr!(); // n 0 /// /// /// # 实现原理 /// 利用宏的重复展开机制把每个表达式替换成固定标记然后统计标记数量。 macro_rules! count_expr { // 匹配规则零个或多个用逗号分隔的表达式 // $($e:expr) —— 重复组每次匹配一个表达式命名为 $e // ,* —— 每个表达式后面跟着逗号零个或多个 () { // 空输入 → 0 个表达式 0usize }; ($($e:expr),* $(,)?) { // 核心技巧 // 1. [()]::len([ $( count_expr!(replace $e) ),* ]) // // 2. 内部 replace 分支把每个表达式替换成固定的 () // 实际上就是 把每个参数换成 ()然后统计 () 数组的长度 // // 3. $(,)? 允许末尾多一个逗号Rust 的宽容语法 [()]::len([ $( count_expr!(replace $e) ),* ]) }; // 内部辅助分支把任意表达式替换成 () (replace $_e:expr) { () }; } // 测试 #[cfg(test)] mod tests { #[test] fn test_count_expr() { assert_eq!(count_expr!(), 0); assert_eq!(count_expr!(a), 1); assert_eq!(count_expr!(1, 2, 3), 3); assert_eq!(count_expr!(hello, 42, true,), 3); // 尾随逗号 } }理解这个宏的关键replace是一个作用域前缀不是 Rust 关键字只是约定表示这是宏内部的辅助规则外部不应该直接调用。核心思想是展开时求值宏展开时[(), (), ()]的.len()是编译期常量不产生运行时开销。四、进阶练习hashmap!宏理解了简单宏后挑战一个更实用的像vec![]一样创建HashMap。// hashmap! 宏 —— 像 vec! 一样方便地创建 HashMap use std::collections::HashMap; /// 创建 HashMap 的宏 /// /// # 用法 /// /// let map hashmap! { /// key1 1, /// key2 2, /// }; /// /// /// # 匹配规则说明 /// /// 核心表达式$( $key:expr $value:expr ),* $(,)? /// /// 逐部分拆解 /// $() —— 重复组的开始 /// $key:expr —— 匹配一个表达式命名为 $key如 key1 /// —— 字面量 分隔符 /// $value:expr —— 匹配一个表达式命名为 $value如 1 /// ) —— 重复组的结束 /// ,* —— 重复组之间用逗号分隔可以出现零次或多次 /// $(,)? —— 可选的尾随逗号 #[macro_export] macro_rules! hashmap { // 分支 1空 HashMap () { ::std::collections::HashMap::new() }; // 分支 2使用冒号语法和 JavaScript 相似 // hashmap! { a: 1, b: 2 } ($($key:ident : $value:expr),* $(,)?) { { let mut _map ::std::collections::HashMap::new(); $( _map.insert(stringify!($key).to_string(), $value); )* _map } }; // 分支 3使用 语法Rust 风格 // hashmap! { a 1, b 2 } ($($key:expr $value:expr),* $(,)?) { { // 创建 HashMap 并预分配容量 let _cap [()]::len([$( hashmap!(count $key, $value) ),*]); let mut _map ::std::collections::HashMap::with_capacity(_cap); // 在展开体中$()* 表示对每个匹配到的 (key, value) 对重复执行 $( _map.insert($key, $value); )* _map } }; // 辅助分支用于预分配容量时的计数每个键值对计数 1 (count $key:expr, $value:expr) { () }; } // 测试用例 fn main() { // 空 map let empty: HashMapString, i32 hashmap!(); assert!(empty.is_empty()); // 字符串 key let scores hashmap! { Alice 95, Bob 87, Charlie 92, }; assert_eq!(scores[Alice], 95); assert_eq!(scores.len(), 3); // 使用标识符作为 key自动转字符串 let config hashmap! { host: localhost, port: 8080, }; assert_eq!(config[host], localhost); assert_eq!(config[port], 8080); }这个宏教给我的三个知识点多分支匹配宏可以有多个匹配规则按定义顺序尝试。第一个匹配的生效。预分配容量像标准库的vec![]一样可以先算出元素个数用with_capacity预分配避免运行时频繁扩容。stringify!的妙用stringify!(a)会把标识符a转成字符串字面量a这样就可以用host: localhost这种简洁语法。五、总结这篇文章介绍了用 AI 辅助学习 Rust 声明宏的方法先让 AI 拆解标准库宏println!、vec!、format!这些你天天用的宏都是最好的教材。让模型逐行解释匹配规则的含义然后用自己的话复述一遍。我的体会是解释给 AI 看让它检查你的理解对不对比单纯听它讲效果更好。从简单开始手写count_expr!是一个仅 6 行的宏但它覆盖了宏的两个核心概念——重复匹配$(...),*和内部辅助规则xxx。写一遍胜过看十遍教程。实用导向学习hashmap!是真实项目中会用到的宏。写它的过程会让你深刻理解展开时求值——with_capacity的容量计算发生在编译期零运行时开销。学宏的感悟不要试图一次性理解所有宏语法。声明宏的设计符expr、ty、tt、ident等很多但日常写宏最常用的就expr和tt两种。先把这两个用熟其他的用到了再查。希望我的学习经验能帮到你。有问题欢迎评论区交流