CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
Rust 语言入门/课时/宏与元编程
RawBlame
课时目录12 节

Rust 语言入门

  1. 01Rust 是什么
  2. 02基础语法
  3. 03所有权与借用
  4. 04生命周期
  5. 05结构体与枚举
  6. 06错误处理
  7. 07Trait 与泛型
  8. 08集合类型
  9. 09并发与 async
  10. 10宏与元编程
  11. 11Web 与生态
  12. 12综合练习

宏与元编程

macro_rules!、derive、DSL 概念。

30 分钟

宏与元编程

这一课你将能做到

  1. 说清宏与函数的区别,熟练使用标准库高频宏(vec!、format!、assert!、dbg!)
  2. 用 macro_rules! 编写声明宏,掌握匹配臂与重复语法
  3. 理解 derive 宏机制,能读懂 serde/thiserror 的用法并知道它们对标 Java 的什么

为什么要学

Rust 代码里到处是感叹号:println!、vec!、assert_eq!。它们是宏——编译期展开为代码的代码。Java 世界里这类需求靠注解 + 反射(运行时付出性能代价)或 Lombok/MapStruct(编译期字节码魔法)实现;Rust 把「编译期生成代码」做成了一等公民,derive 宏就是它的 Lombok,而且功能强得多。

工程上的实际意义:serde 一个 #[derive(Serialize)] 生成的序列化代码是手写级别的性能(无反射),判题接口的 JSON 编解码零成本——这就是第 11 课 axum 开发的地基。

先建立图像

Java 注解体系                        Rust 宏体系
@interface + 运行时反射        ↔    过程宏(编译期生成代码,无反射)
Lombok @Getter/@Builder        ↔    #[derive(Debug, Clone, ...)]
MapStruct @Mapper              ↔    derive 宏 / 函数式过程宏
varargs / String.format        ↔    macro_rules! 声明宏(可变参数、编译期检查)

两类宏的分工:

  • 声明宏(macro_rules!):模式匹配替换,写「语法糖工具」,如自定义初始化语法
  • 过程宏(proc-macro):拿到 TokenStream 直接操作,分 derive / 属性 / 函数式三种,生态重器(serde、thiserror、tokio::main)全是它

跟着做

步骤 1:先认全高频宏

cargo new macro-demo
cd macro-demo
#[derive(Debug)]
struct Case { id: u32, time_ms: u32 }

fn main() {
    let cases = vec![                          // vec!:字面量建 Vec
        Case { id: 1, time_ms: 120 },
        Case { id: 2, time_ms: 990 },
    ];

    println!("{cases:?}");                     // println!:格式化输出,{:?} 走 Debug
    println!("{} 最慢 {} ms", cases.len(), cases.iter().map(|c| c.time_ms).max().unwrap());

    let summary = format!("共 {} 个用例", cases.len());   // format!:返回 String
    println!("{summary}");

    assert_eq!(cases.len(), 2);                // assert!/assert_eq!:测试断言
    dbg!(&cases[0]);                           // dbg!:打印文件行号 + 表达式 + 值,调试利器

    let max = cases.iter().map(|c| c.time_ms).max().unwrap_or(0);
    println!("最长 {max} ms");
}

dbg! 对标 IDE 里的「打印调试但带定位」:输出文件名、行号、表达式原文和值,比 println! 调试效率高一个档位。

步骤 2:第一个声明宏

macro_rules! hash_map {
    // 匹配臂:零或多组「表达式 => 表达式」,允许尾逗号
    ( $( $k:expr => $v:expr ),* $(,)? ) => {{
        let mut m = std::collections::HashMap::new();
        $( m.insert($k, $v); )*            // 对每组匹配展开一次 insert
        m
    }};
}

fn main() {
    let limits = hash_map! {
        "cpp" => 1000u32,
        "java" => 2000,
        "python" => 5000,
    };
    println!("{limits:?}");
    println!("python 时限 {}", limits["python"]);
}

读法:$( ... ),* 是「重复片段 + 分隔符」,$(,)? 是可选的尾逗号。展开发生在编译期、按 token 精确匹配——类型错误会在展开后的代码里被正常检查,没有任何运行时魔法。

步骤 3:带多种捕获的声明宏

macro_rules! judge_log {
    // 两条匹配臂:宏按传入形式自动选路,编译期分派
    ($id:expr, pass, $ms:expr) => {
        println!("[submit-{}] => AC,{} ms", $id, $ms);
    };
    ($id:expr, fail, $reason:expr) => {
        println!("[submit-{}] => 失败:{}", $id, $reason);
    };
}

fn main() {
    judge_log!(1, pass, 128);
    judge_log!(2, fail, "WA on case 3");
}

捕获种类:expr(表达式)、ident(标识符)、literal(字面量)、tt(单棵语法树)。宏不是文本替换,是 token 树替换——因此没有 C 宏的副作用灾难,且天然卫生(宏内变量不会泄漏污染调用方作用域)。

步骤 4:derive 宏——一行生成实现

use std::fmt;

#[derive(Debug, Clone, PartialEq, Default)]
struct Submission {
    id: u64,
    language: String,
    time_used_ms: u32,
}

// Debug 手写版长这样——derive 帮你省掉的全是这种模板代码
impl fmt::Debug for Submission2 {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("Submission2")
            .field("id", &self.id)
            .field("language", &self.language)
            .finish()
    }
}

struct Submission2 { id: u64, language: String }

fn main() {
    let a = Submission::default();
    let b = a.clone();
    println!("{a:?} 相等 {}", a == b);
    println!("{:?}", Submission2 { id: 1, language: "cpp".into() });
}

#[derive(Debug, Clone, PartialEq, Default)] 一行生成四个 impl。derive 宏在编译期读取结构体字段信息,逐字段生成代码——这正是第 11 课 serde 序列化、第 6 课 thiserror 错误的底层机制。

步骤 5:生态级 derive 实战预演

[dependencies]
serde = { version = "1", features = ["derive"] }
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, Debug)]
struct SubmitReq {
    #[serde(rename = "problemId")]      // 属性宏式配置:字段重命名
    problem_id: u64,
    language: String,
    #[serde(default)]                    // 缺字段时用 Default
    priority: u8,
}

fn main() {
    let json = r#"{"problemId": 1001, "language": "rust"}"#;
    let req: SubmitReq = serde_json::from_str(json).unwrap();
    println!("{req:?}");

    let out = serde_json::to_string_pretty(&req).unwrap();
    println!("{out}");
}

#[derive(Serialize, Deserialize)] 让 serde 在编译期生成该结构体专用的 JSON 编解码器——零反射、零运行时注册。对标 Java Jackson 的注解 + 反射,性能差一个数量级。

讲清楚要点

  1. 宏的判定标志是感叹号:println! 是宏,format! 也是;宏接受不定数量的 token,函数做不到的事它做。
  2. 声明宏的骨架:macro_rules! 名字 { (模式) => { 展开 }; },多臂匹配从上到下尝试;捕获用 $name:kind,重复用 $(...),*。
  3. 宏卫生:宏展开里的标识符不会意外捕获调用方的同名变量(除非显式传入),比 C 宏安全一个时代。
  4. 过程宏三分类:derive(挂在类型上生成 impl)、属性宏(#[tokio::main] 改写整个函数)、函数式宏(sqlx::query! 编译期校验 SQL)。
  5. 编译期 vs 运行期的分界线:Rust 宏在编译期展开成普通代码再走类型检查;Java 注解反射在运行期读取——这是 serde 快于 Jackson 的根本原因。

你可能会懵的地方

  • 宏和泛型怎么选? 泛型约束「同一段逻辑适配多种类型」;宏适配「不同代码形状/语法」(如 vec! 接受任意数量元素)。能用泛型先用泛型,类型系统还帮你检查;宏是语法层工具,类型检查发生在展开之后。

  • 为什么宏必须先定义后使用? macro_rules! 按 crate 内文本顺序生效(use 之前要 macro 处于作用域)。同 crate 内把宏定义放到 lib.rs 顶部或用 #[macro_use] 模块声明;过程宏则没有顺序限制。

  • derive 生成的代码出错了怎么调? 报错位置在展开后的代码,cargo expand(cargo install cargo-expand)能看到完整展开结果。90% 的情况是字段类型不支持对应 trait(如 Vec 嵌套缺 derive)。

  • 宏能写业务逻辑吗? 能但不该。宏的可读性和 IDE 支持弱于函数/泛型,只用于「语法糖、样板消除、DSL」三类场景;业务逻辑用函数和 trait。

  • thiserror/anyhow 和手写 impl Display 什么关系? thiserror 是 derive 宏,替你生成第 6 课手写的 Display + From 三件套;anyhow 是运行时类型擦除的错误袋。手写一次理解原理,生产用宏省样板。

错法对照

错法问题正解
调用宏漏写感叹号 vec[1,2]被当索引/数组语法报错vec![1, 2]
宏定义放在使用之后macro 未定义先使用定义提到文件顶部,或按 crate 加载顺序组织
重复分隔符写错 $(...)* ,展开的 token 流不对分隔符在 * 之前:$(...),*
derive 的 trait 没开 featureserde 报 derive 不可用serde = { version = "1", features = ["derive"] }
宏内随手引用外部变量卫生规则导致找不到变量通过宏参数 $x:expr 显式传入

卡点急救

  1. error: expected expression, found =>:宏匹配臂语法错了。检查 $( $k:expr => $v:expr ),* 的捕获种类与分隔符位置;从最简单的单臂宏开始加复杂度。
  2. 编译错误指向宏展开内部:cargo expand 看展开产物定位真凶;确认每个捕获的 kind(expr/ident/tt)与传参形式一致。
  3. derive 报 "cannot find trait":依赖 feature 未开启(serde derive、tokio macros),核对 Cargo.toml;自定义 derive 需要单独的 proc-macro crate。

面试常问

  • 声明宏和过程宏的区别? 声明宏基于模式匹配做 token 替换,语法简单、适合小型语法糖;过程宏拿到完整 TokenStream 用 Rust 代码操作,能生成任意代码,derive/属性/函数式三种。serde、tokio::main 都是过程宏。

  • derive 宏和 Java 注解处理器的对比? 两者都编译期生效,但 derive 直接输出 Rust 代码参与后续类型检查,无需字节码插桩;Java 反射注解走运行时元数据查询。Lombok 更接近 derive,但通过修改 AST/字节码实现,调试体验差于 Rust 的 cargo expand。

  • 为什么 serde 比 Jackson 快? 编译期生成结构体专用的序列化代码:字段偏移在编译期确定,无反射查找、无 Map 装箱、可内联;Jackson 运行时反射 + 缓存。这是「元编程前移到编译期」的典型性能红利。

自测三问

  1. $( $k:expr => $v:expr ),* $(,)? 每一段各是什么含义?
  2. 宏的「卫生性」指什么?它避免了 C 宏的哪类灾难?
  3. serde 的 #[serde(rename = "...")] 属于哪一类宏机制?

小结一句话

宏是 Rust 的编译期代码生成器:声明宏做语法糖,derive 宏做样板消除——Java 靠反射和 Lombok 绕的弯路,Rust 在编译期一次走直。

延伸

  • Rust by Example:宏
  • 下一课:Web 与生态——axum + serde 搭一个高频判题提交接口。
加载本节练习…
← 上一课并发与 async下一课 →Web 与生态