Rust Macros: From Preprocessor to Metaprogramming
Rust 宏:从预处理器到元编程
What you’ll learn: How Rust macros work, when to use them instead of functions or generics, and how they replace the C/C++ preprocessor. By the end of this chapter you will be able to write your own
macro_rules!macros and understand what#[derive(Debug)]is really generating for you.
本章将学到什么: Rust 宏到底是怎么工作的,什么时候该用宏而不是函数或泛型,以及它是怎样取代 C/C++ 预处理器那一套的。学完这一章之后,就能自己写macro_rules!宏,也能看明白#[derive(Debug)]背后到底生成了什么代码。
Macros are one of the very first things people see in Rust, for example println!("hello"), but却常常是课程里最晚才解释清楚的部分。本章就是专门来补这个坑的。
宏明明出场很早,却总被拖到最后才讲,这确实挺别扭。本章就是把这件事一次讲透。
Why Macros Exist
为什么会有宏
Functions and generics already handle most code reuse in Rust. Macros exist to cover the places where the type system and ordinary functions触不到。
也就是说,宏不是拿来滥用的,而是用来补函数和泛型做不到的那几块。
| Need | Function/Generic? | Macro? | Why |
|---|---|---|---|
| Compute a value | ✅ fn max<T: Ord>(a: T, b: T) -> T | — | Type system handles it 普通函数和泛型足够了 |
| Accept variable number of arguments | ❌ Rust has no variadic functions | ✅ println!("{} {}", a, b) | Macros can accept an arbitrary token list 宏可以吃任意数量的 token |
Generate repetitive impl blocks | ❌ Not possible with generics alone | ✅ macro_rules! | Macros generate source code at compile time 宏能在编译期直接生成代码 |
| Run code at compile time | ❌ const fn is limited | ✅ Procedural macros | Full Rust code can run during compilation 过程宏能在编译期跑真正的 Rust 逻辑 |
| Conditionally include code | ❌ | ✅ #[cfg(...)] | Attribute-style macros and cfg drive compilation 属性宏和条件编译控制代码是否存在 |
If coming from C/C++, the right mental model is: Rust macros are the only sane replacement for the preprocessor. The difference is that they operate on syntax trees instead of raw text, so they are hygienic and type-aware.
从 C/C++ 视角看,Rust 宏可以理解成“正确版本的预处理器替代品”。区别在于它处理的是语法结构,不是纯文本替换,所以不会轻易发生命名污染,也更容易和类型系统配合。
For C developers: Rust macros replace
#definecompletely. There is no textual preprocessor. See ch18 for the full preprocessor-to-Rust mapping.
给 C 开发者: Rust 没有那种文本级预处理器,#define这套思路整体被宏体系取代了。更完整的预处理器映射关系可以看 ch18。
Declarative Macros with macro_rules!
声明式宏:macro_rules!
Declarative macros, also called macros by example, are the most common macro form in Rust. They work by pattern-matching on syntax, much like match works on values.
声明式宏也叫“按样例匹配的宏”,是 Rust 里最常见的宏形式。它的工作方式很像 match,只不过匹配对象从运行时的值换成了语法结构。
Basic syntax
基础语法
macro_rules! say_hello {
() => {
println!("Hello!");
};
}
fn main() {
say_hello!(); // Expands to: println!("Hello!");
}
The ! after the name is the signal to both the compiler and the reader that this is a macro invocation, not an ordinary function call.
名字后面那个 ! 就是在明确告诉编译器和读代码的人:这不是函数调用,这是宏展开。
Pattern matching with arguments
带参数的模式匹配
Macros match token trees via fragment specifiers.
宏通过 fragment specifier 去匹配 token tree,不是按字符串硬替换。
macro_rules! greet {
// Pattern 1: no arguments
() => {
println!("Hello, world!");
};
// Pattern 2: one expression argument
($name:expr) => {
println!("Hello, {}!", $name);
};
}
fn main() {
greet!(); // "Hello, world!"
greet!("Rust"); // "Hello, Rust!"
}
Fragment specifiers reference
fragment specifier 速查
| Specifier | Matches | Example |
|---|---|---|
$x:expr | Any expression 任意表达式 | 42, a + b, foo() |
$x:ty | A type 一个类型 | i32, Vec<String>, &str |
$x:ident | An identifier 标识符 | foo, my_var |
$x:pat | A pattern 模式 | Some(x), _, (a, b) |
$x:stmt | A statement 语句 | let x = 5; |
$x:block | A block 代码块 | { println!("hi"); 42 } |
$x:literal | A literal 字面量 | 42, "hello", true |
$x:tt | A single token tree 单个 token tree | Almost anything |
$x:item | An item like fn / struct / impl条目定义 | fn foo() {} |
Repetition — the killer feature
重复匹配:最有杀伤力的能力
C/C++ 宏做不到循环展开这种事,而 Rust 宏可以直接重复一段模式。
这也是为什么很多样板代码在 Rust 里适合交给宏处理。
macro_rules! make_vec {
// Match zero or more comma-separated expressions
( $( $element:expr ),* ) => {
{
let mut v = Vec::new();
$( v.push($element); )* // Repeat for each matched element
v
}
};
}
fn main() {
let v = make_vec![1, 2, 3, 4, 5];
println!("{v:?}"); // [1, 2, 3, 4, 5]
}
The syntax $( ... ),* means “match zero or more repetitions of this pattern separated by commas.” The expansion-side $( ... )* then repeats the body once for each matched element.$( ... ),* 的意思是“匹配零个或多个、以逗号分隔的模式项”;展开侧的 $( ... )* 则表示“每匹配到一个,就把这里复制一遍”。
This is exactly how
vec![]is implemented in the standard library. The real source is close to the following:
标准库里的vec![]本质上就是这么实现的。 实际源码形式和下面非常接近:#![allow(unused)] fn main() { macro_rules! vec { () => { Vec::new() }; ($elem:expr; $n:expr) => { vec::from_elem($elem, $n) }; ($($x:expr),+ $(,)?) => { <[_]>::into_vec(Box::new([$($x),+])) }; } }The trailing
$(,)?means an optional trailing comma is accepted.
最后那个$(,)?就是在允许“多写一个尾逗号”。
Repetition operators
重复运算符
| Operator | Meaning | Example |
|---|---|---|
$( ... )* | Zero or more 零个或多个 | vec![], vec![1], vec![1, 2, 3] |
$( ... )+ | One or more 一个或多个 | At least one element required |
$( ... )? | Zero or one 零个或一个 | Optional trailing item |
Practical example: a hashmap! constructor
实用例子:自己写个 hashmap! 构造器
The standard library gives you vec![] but no built-in hashmap!{}. Writing one is a good demonstration of pattern repetition.
标准库有 vec![],却没有内置 hashmap!{}。自己写一个,正好能把模式重复的威力看明白。
macro_rules! hashmap {
( $( $key:expr => $value:expr ),* $(,)? ) => {
{
let mut map = std::collections::HashMap::new();
$( map.insert($key, $value); )*
map
}
};
}
fn main() {
let scores = hashmap! {
"Alice" => 95,
"Bob" => 87,
"Carol" => 92, // trailing comma OK thanks to $(,)?
};
println!("{scores:?}");
}
Practical example: diagnostic check macro
实用例子:诊断检查宏
A common embedded or systems pattern is “check a condition, and if it fails return an error immediately.” This is a good fit for a macro.
嵌入式和系统代码里,经常会有“条件不满足就立刻返回错误”的模式,这种场景很适合用宏抽出来。
#![allow(unused)]
fn main() {
use thiserror::Error;
#[derive(Error, Debug)]
enum DiagError {
#[error("Check failed: {0}")]
CheckFailed(String),
}
macro_rules! diag_check {
($cond:expr, $msg:expr) => {
if !($cond) {
return Err(DiagError::CheckFailed($msg.to_string()));
}
};
}
fn run_diagnostics(temp: f64, voltage: f64) -> Result<(), DiagError> {
diag_check!(temp < 85.0, "GPU too hot");
diag_check!(voltage > 0.8, "Rail voltage too low");
diag_check!(voltage < 1.5, "Rail voltage too high");
println!("All checks passed");
Ok(())
}
}
C/C++ comparison:
和 C/C++ 的对照:// C preprocessor — textual substitution, no type safety, no hygiene #define DIAG_CHECK(cond, msg) \ do { if (!(cond)) { log_error(msg); return -1; } } while(0)The Rust version returns a proper
Result, avoids double evaluation traps, and the compiler verifies that$condis a valid boolean expression.
Rust 版本会返回正规的Result,没有那种宏参数被重复求值的坑,而且编译器还会检查$cond真的是个布尔表达式。
Hygiene: why Rust macros are safer
卫生性:为什么 Rust 宏更安全
C/C++ 宏最容易出事的点之一,就是名字碰撞和副作用重复求值。
这也是很多人一提宏就头大的根源。
// C: dangerous — `x` could shadow the caller's `x`
#define SQUARE(x) ((x) * (x))
int x = 5;
int result = SQUARE(x++); // UB: x incremented twice!
Rust macros are hygienic, which means variables introduced inside the macro body do not accidentally collide with names from the call site.
Rust 宏具有卫生性,也就是宏内部引入的标识符,不会随便污染调用点的命名空间。
macro_rules! make_x {
() => {
let x = 42; // This `x` is scoped to the macro expansion
};
}
fn main() {
let x = 10;
make_x!();
println!("{x}"); // Prints 10, not 42 — hygiene prevents collision
}
The macro’s x and the caller’s x are treated as distinct bindings by the compiler. That level of hygiene simply does not exist in the C preprocessor world.
宏里的 x 和外面那个 x 在编译器眼里根本就不是一回事。C 预处理器那种纯文本替换,做不到这种防护。
Common Standard Library Macros
标准库里那些常见宏
这些宏从第一章就开始用了,只是前面没有专门拆开说。
现在正好把它们的作用一起捋顺。
| Macro | What it does | Expands to, simplified |
|---|---|---|
println!("{}", x) | Format and print to stdout with a newline 格式化后打印到标准输出并换行 | std::io::_print(format_args!(...)) |
eprintln!("{}", x) | Print to stderr with a newline 打印到标准错误并换行 | Same idea, different output stream |
format!("{}", x) | Format into a String格式化成一个 String | Allocates and returns a String |
vec![1, 2, 3] | Construct a Vec with elements构造一个向量 | Approximately Vec::from([1, 2, 3]) |
todo!() | Mark unfinished code 标记尚未完成的代码 | panic!("not yet implemented") |
unimplemented!() | Mark deliberately missing implementation 标记故意暂未实现 | panic!("not implemented") |
unreachable!() | Mark code that should never execute 标记理论上不该走到的路径 | panic!("unreachable") |
assert!(cond) | Panic if condition is false 条件不成立就 panic | if !cond { panic!(...) } |
assert_eq!(a, b) | Panic if values differ 值不相等就 panic | Also prints both sides on failure |
dbg!(expr) | Print expression and value to stderr, then return the value 把表达式和值打到 stderr,再把值原样返回 | Debug helper |
include_str!("file.txt") | Embed a file as &str at compile time编译期把文件内容嵌成字符串 | Reads the file during compilation |
include_bytes!("data.bin") | Embed a file as &[u8] at compile time编译期把文件内容嵌成字节数组 | Reads the file during compilation |
cfg!(condition) | Evaluate a compile-time condition into bool把条件编译判断变成布尔值 | true or false |
env!("VAR") | Read an environment variable at compile time 编译期读取环境变量 | Compilation fails if missing |
concat!("a", "b") | Concatenate literals at compile time 编译期拼接字面量 | "ab" |
dbg! — the debugging macro you’ll use all the time
dbg!:日常排查时非常顺手的宏
fn factorial(n: u32) -> u32 {
if dbg!(n <= 1) { // Prints: [src/main.rs:2] n <= 1 = false
dbg!(1) // Prints: [src/main.rs:3] 1 = 1
} else {
dbg!(n * factorial(n - 1)) // Prints intermediate values
}
}
fn main() {
dbg!(factorial(4)); // Prints all recursive calls with file:line
}
dbg! returns the wrapped value, so it can be inserted without changing the surrounding logic. It writes to stderr rather than stdout, so it usually does not disturb normal program output.dbg! 的妙处在于它会把包住的值原样返回,所以往表达式中间塞进去也不会改变程序结构。它打印到 stderr,因此通常不会搅乱正常输出。
Remove all dbg! calls before committing.
正式提交前,dbg! 最好都清干净,别把调试痕迹留在主代码里。
Format string syntax
格式化字符串语法速查
Since println!、format!、eprintln! and write! all share the same formatting machinery, the quick reference below applies to all of them.println!、format!、eprintln!、write! 底层都共用一套格式化系统,所以这张速查表基本都适用。
#![allow(unused)]
fn main() {
let name = "sensor";
let value = 3.14159;
let count = 42;
println!("{name}"); // Variable by name (Rust 1.58+)
println!("{}", name); // Positional
println!("{value:.2}"); // 2 decimal places: "3.14"
println!("{count:>10}"); // Right-aligned, width 10: " 42"
println!("{count:0>10}"); // Zero-padded: "0000000042"
println!("{count:#06x}"); // Hex with prefix: "0x002a"
println!("{count:#010b}"); // Binary with prefix: "0b00101010"
println!("{value:?}"); // Debug format
println!("{value:#?}"); // Pretty-printed Debug format
}
For C developers: Think of this as a type-safe
printf; the compiler checks that the formatting directives match the argument types.
给 C 开发者: 可以把它看成类型安全版printf。像%s配整数、%d配字符串这种错,Rust 会在编译期拦下来。For C++ developers: This replaces a lot of
std::cout << ... << std::setprecision(...)ceremony with one format string.
给 C++ 开发者: 它基本取代了那种一长串std::cout <<配std::setprecision的组合拳,写法更集中。
Derive Macros
派生宏
This book has already used #[derive(...)] on almost every struct and enum.
前面一路看到的 #[derive(...)],本质上就是派生宏最典型的例子。
#![allow(unused)]
fn main() {
#[derive(Debug, Clone, PartialEq)]
struct Point {
x: f64,
y: f64,
}
}
#[derive(Debug)] is a special kind of procedural macro. It inspects the type definition at compile time and generates the corresponding trait implementation automatically.#[derive(Debug)] 属于过程宏的一种。它会在编译期读入类型定义,然后自动生成对应 trait 的实现。
#![allow(unused)]
fn main() {
// What #[derive(Debug)] generates for Point:
impl std::fmt::Debug for Point {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Point")
.field("x", &self.x)
.field("y", &self.y)
.finish()
}
}
}
Without #[derive(Debug)], you would have to write that whole impl by hand for every type.
如果没有派生宏,这种样板实现每个结构体都得手写一遍,想想就够烦。
Commonly derived traits
常见的派生 trait
| Derive | What it generates | When to use |
|---|---|---|
Debug | {:?} formatting调试输出格式 | Almost always useful 几乎总是值得加 |
Clone | .clone() support显式复制能力 | When values need duplication |
Copy | Implicit copy on assignment 赋值时按值复制 | Small stack-only types |
PartialEq / Eq | Equality comparison 相等比较 | Types that should compare by value |
PartialOrd / Ord | Ordering support 排序和比较能力 | Types with meaningful ordering |
Hash | Hashing support 哈希能力 | Hash map / hash set keys |
Default | Type::default()默认值构造 | Types with sensible zero or empty state |
Serialize / Deserialize | Serialization support 序列化与反序列化 | API and persistence boundary types |
The derive decision tree
该不该派生,怎么判断
Should I derive it?
│
├── Does my type contain only types that implement the trait?
│ ├── Yes → #[derive] will work
│ └── No → Write a manual impl (or skip it)
│
└── Will users of my type reasonably expect this behavior?
├── Yes → Derive it (Debug, Clone, PartialEq are almost always reasonable)
└── No → Don't derive (e.g., don't derive Copy for a type with a file handle)
C++ comparison:
#[derive(Clone)]is like auto-generating a correct copy constructor, and#[derive(PartialEq)]is close to auto-generating field-wise equality. Modern C++ has started moving in that direction, but Rust makes it far more routine.
和 C++ 的类比:#[derive(Clone)]有点像自动生成正确的拷贝构造,#[derive(PartialEq)]则像自动生成按字段比较的operator==。现代 C++ 也在往这个方向靠,但 Rust 把它做成了日常操作。
Attribute Macros
属性宏
Attribute macros transform the item they annotate. In practice, the book has already used several of them.
属性宏会改写它挂着的那个条目。前面其实已经用过不少,只是当时没有专门点名。
#![allow(unused)]
fn main() {
#[test] // Marks a function as a test
fn test_addition() {
assert_eq!(2 + 2, 4);
}
#[cfg(target_os = "linux")] // Conditionally includes this function
fn linux_only() { /* ... */ }
#[derive(Debug)] // Generates Debug implementation
struct MyType { /* ... */ }
#[allow(dead_code)] // Suppresses a compiler warning
fn unused_helper() { /* ... */ }
#[must_use] // Warn if return value is discarded
fn compute_checksum(data: &[u8]) -> u32 { /* ... */ }
}
Common built-in attributes:
常见内建属性如下:
| Attribute | Purpose |
|---|---|
#[test] | Mark a test function 标记测试函数 |
#[cfg(...)] | Conditional compilation 条件编译 |
#[derive(...)] | Auto-generate trait impls 自动生成 trait 实现 |
#[allow(...)] / #[deny(...)] / #[warn(...)] | Control lint levels 控制 lint 级别 |
#[must_use] | Warn on ignored return values 返回值被忽略时发警告 |
#[inline] / #[inline(always)] | Hint inlining behavior 提示内联 |
#[repr(C)] | C-compatible layout 保证 C 兼容布局 |
#[no_mangle] | Preserve symbol name 保持导出符号名 |
#[deprecated] | Mark deprecated items 标记废弃接口 |
For C/C++ developers: Attributes replace a weird mixture of pragmas, compiler-specific attributes, and preprocessor tricks. The nice part is that they are part of Rust’s actual syntax rather than bolt-on hacks.
给 C/C++ 开发者: 这套属性机制,本质上取代了 pragma、编译器专属 attribute、以及部分预处理器技巧的混搭局面。好处是它们属于语言正经语法的一部分,不是外挂补丁。
Procedural Macros
过程宏
Procedural macros are separate Rust programs that run at compile time and generate code. They are more powerful than macro_rules!, but also more complex and heavier to write.
过程宏本质上是“编译期运行的 Rust 程序”。它比 macro_rules! 更强,但复杂度也高不少,不是拿来随手乱上的。
There are three kinds:
过程宏主要分三类:
| Kind | Syntax | Example | What it does |
|---|---|---|---|
| Function-like | my_macro!(...) | sql!(SELECT * FROM users) | Parse custom syntax and generate Rust code 解析自定义语法并生成 Rust 代码 |
| Derive | #[derive(MyTrait)] | #[derive(Serialize)] | Generate a trait impl from a type definition 根据类型定义生成 trait 实现 |
| Attribute | #[my_attr] | #[tokio::main], #[instrument] | Transform the annotated item 改写被标注的函数或类型 |
You have already used proc macros
其实已经用过过程宏了
#[derive(Error)]fromthiserrorgeneratesDisplayandFromimplementations for error enums.thiserror里的#[derive(Error)]会帮错误枚举生成Display和From相关实现。#[derive(Serialize, Deserialize)]fromserdegenerates serialization and deserialization code.serde的这两个派生宏会自动生成序列化和反序列化逻辑。#[tokio::main]rewritesasync fn main()into runtime setup plusblock_onmachinery.#[tokio::main]会把异步入口函数改写成运行时初始化加执行包装。#[test]is also effectively part of this compile-time registration machinery.#[test]也可以看成这类“编译期登记和改写”的一部分。
When to write your own proc macro
什么时候需要自己写过程宏
During normal application development, writing a custom proc macro is not common. Reach for it when:
正常业务开发里,自己动手写过程宏并不算高频操作。一般是遇到下面这些需求时才值得考虑:
- You need to inspect struct fields or enum variants at compile time.
需要在编译期读取结构体字段或枚举变体信息。 - You are building a domain-specific language.
需要做一套领域特定语法。 - You need to transform function signatures or wrap functions systematically.
需要批量改写函数签名或给函数统一包一层逻辑。
For most day-to-day code, macro_rules! or a plain generic function is still the better choice.
大多数日常代码场景里,macro_rules! 或普通函数就够了,别动不动就把武器升级过头。
C++ comparison: Procedural macros occupy a space similar to code generators, heavy template metaprogramming, or external tools like
protoc. The key difference is that Rust integrates them directly into the Cargo build pipeline.
和 C++ 的类比: 过程宏有点像代码生成器、重型模板元编程,或者protoc这类外部工具。最大的区别是 Rust 把它们直接纳入 Cargo 构建链里,不需要额外拼装那么多外部步骤。
When to Use What: Macros vs Functions vs Generics
到底该用宏、函数,还是泛型
Need to generate code?
│
├── No → Use a function or generic function
│ (simpler, better error messages, IDE support)
│
└── Yes ─┬── Variable number of arguments?
│ └── Yes → macro_rules! (e.g., println!, vec!)
│
├── Repetitive impl blocks for many types?
│ └── Yes → macro_rules! with repetition
│
├── Need to inspect struct fields?
│ └── Yes → Derive macro (proc macro)
│
├── Need custom syntax (DSL)?
│ └── Yes → Function-like proc macro
│
└── Need to transform a function/struct?
└── Yes → Attribute proc macro
General guideline: if a normal function or generic function can solve the problem, prefer that. Macros usually have worse error messages, are harder to debug, and IDE support inside macro bodies is often weaker.
总体原则: 只要普通函数或泛型函数能解决,就先用它们。宏的错误信息通常更拧巴,调试体验也更差,IDE 支持也没那么丝滑。
Exercises
练习
🟢 Exercise 1: min! macro
🟢 练习 1:实现 min! 宏
Write a min! macro that:
写一个 min! 宏,要求如下:
min!(a, b)returns the smaller of two values.min!(a, b)返回两个值里更小的那个。min!(a, b, c)returns the smallest of three values.min!(a, b, c)返回三个值里最小的那个。- It works for any type implementing
PartialOrd.
凡是实现了PartialOrd的类型都能用。
Hint: You will need two match arms in macro_rules!.
提示: 这个宏至少需要两个分支匹配臂。
Solution 参考答案
macro_rules! min {
($a:expr, $b:expr) => {
if $a < $b { $a } else { $b }
};
($a:expr, $b:expr, $c:expr) => {
min!(min!($a, $b), $c)
};
}
fn main() {
println!("{}", min!(3, 7)); // 3
println!("{}", min!(9, 2, 5)); // 2
println!("{}", min!(1.5, 0.3)); // 0.3
}
Note: In production code, prefer std::cmp::min or methods like a.min(b). This exercise is mainly about understanding multi-arm macro expansion.
说明: 真到生产代码里,优先还是用 std::cmp::min 或类似 a.min(b) 的现成方法。这里主要是为了练多分支宏的写法。
🟡 Exercise 2: hashmap! from scratch
🟡 练习 2:从零写一个 hashmap!
Without looking back at the earlier example, write a hashmap! macro that:
先别回头抄前面的例子,自己写一个 hashmap!,要求如下:
- Creates a
HashMapfromkey => valuepairs.
能够根据key => value形式的输入构造HashMap。 - Supports trailing commas.
支持尾逗号。 - Works with any key type that implements hashing.
只要 key 是可哈希类型,都能用。
Test with:
测试用例如下:
#![allow(unused)]
fn main() {
let m = hashmap! {
"name" => "Alice",
"role" => "Engineer",
};
assert_eq!(m["name"], "Alice");
assert_eq!(m.len(), 2);
}
Solution 参考答案
use std::collections::HashMap;
macro_rules! hashmap {
( $( $key:expr => $val:expr ),* $(,)? ) => {{
let mut map = HashMap::new();
$( map.insert($key, $val); )*
map
}};
}
fn main() {
let m = hashmap! {
"name" => "Alice",
"role" => "Engineer",
};
assert_eq!(m["name"], "Alice");
assert_eq!(m.len(), 2);
println!("Tests passed!");
}
🟡 Exercise 3: assert_approx_eq! for floating-point comparison
🟡 练习 3:给浮点比较写个 assert_approx_eq!
Write a macro assert_approx_eq!(a, b, epsilon) that panics if |a - b| > epsilon. This is useful in tests where exact floating-point equality is unrealistic.
写一个宏 assert_approx_eq!(a, b, epsilon),当 |a - b| > epsilon 时触发 panic。浮点数测试里经常需要这种“近似相等”判断。
Test with:
可以用下面这些例子测试:
#![allow(unused)]
fn main() {
assert_approx_eq!(0.1 + 0.2, 0.3, 1e-10); // Should pass
assert_approx_eq!(3.14159, std::f64::consts::PI, 1e-4); // Should pass
// assert_approx_eq!(1.0, 2.0, 0.5); // Should panic
}
Solution 参考答案
macro_rules! assert_approx_eq {
($a:expr, $b:expr, $eps:expr) => {
let (a, b, eps) = ($a as f64, $b as f64, $eps as f64);
let diff = (a - b).abs();
if diff > eps {
panic!(
"assertion failed: |{} - {}| = {} > {} (epsilon)",
a, b, diff, eps
);
}
};
}
fn main() {
assert_approx_eq!(0.1 + 0.2, 0.3, 1e-10);
assert_approx_eq!(3.14159, std::f64::consts::PI, 1e-4);
println!("All float comparisons passed!");
}
🔴 Exercise 4: impl_display_for_enum!
🔴 练习 4:实现 impl_display_for_enum!
Write a macro that generates a Display implementation for simple C-like enums. Given the following invocation:
写一个宏,用来给简单的 C 风格枚举生成 Display 实现。假设调用形式如下:
#![allow(unused)]
fn main() {
impl_display_for_enum! {
enum Color {
Red => "red",
Green => "green",
Blue => "blue",
}
}
}
It should generate both the enum definition and the matching impl Display block that maps each variant to its string form.
它应该同时生成 enum Color { ... } 的定义,以及相应的 impl Display for Color,把每个变体映射到指定字符串。
Hint: You will need both repetition and several fragment specifiers.
提示: 这里既会用到重复模式,也会用到多个 fragment specifier。
Solution 参考答案
use std::fmt;
macro_rules! impl_display_for_enum {
(enum $name:ident { $( $variant:ident => $display:expr ),* $(,)? }) => {
#[derive(Debug, Clone, Copy, PartialEq)]
enum $name {
$( $variant ),*
}
impl fmt::Display for $name {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
$( $name::$variant => write!(f, "{}", $display), )*
}
}
}
};
}
impl_display_for_enum! {
enum Color {
Red => "red",
Green => "green",
Blue => "blue",
}
}
fn main() {
let c = Color::Green;
println!("Color: {c}"); // "Color: green"
println!("Debug: {c:?}"); // "Debug: Green"
assert_eq!(format!("{}", Color::Red), "red");
println!("All tests passed!");
}