Rust高级之宏讲解

1 宏

1.1 简介

宏(Macros)是一种在编译时生成代码的强大工具,它允许在编写代码时创建自定义语法扩展。宏(Macro)是一种在代码中进行元编程(Metaprogramming)的技术,允许在编译时生成代码,宏可以帮助简化代码,提高代码的可读性和可维护性,同时允许开发者在编译时执行一些代码生成的操作。

宏在 Rust 中有两种类型:声明式宏(Declarative Macros)和过程宏(Procedural Macros)。

1.2 声明式宏

1.2.1 定义

在 Rust 中,使用 macro_rules! 关键字来定义声明式宏。

macro_rules! my_macro {
    ($arg:expr) => {// 模式匹配和展开
        // 生成的代码
        // 使用 $arg 来代替匹配到的表达式
    };
}

声明式宏使用 macro_rules! 关键字进行定义,它们被称为 macro_rules 宏,这种宏的定义是基于模式匹配的,可以匹配代码的结构并根据匹配的模式生成相应的代码。这样的宏在不引入新的语法结构的情况下,可以用来简化一些通用的代码模式。

片段说明符:
$arg:expr 里的 expr不是随便写的名字,是预定义的语法类别:

macro_rules! demo {
 ($e:expr) => { ... }; //匹配任意表达式:1+2, foo(), x 
 ($i:ident) => { ... }; //匹配标识符:变量名、函数名 
 ($t:ty) => { ... }; //匹配类型:i32, Vec<String>
 ($b:block) => { ... }; //匹配代码块:{ ... }
 ($s:stmt) => { ... }; //匹配语句 
 ($p:pat) => { ... }; //匹配模式:Some(x), (a, b)
 ($l:literal) => { ... }; //匹配字面量:42, "abc"
 ($v:vis) => { ... }; //匹配可见性:pub, pub(crate)
 ($m:meta) => { ... }; //匹配属性内容:derive(Debug)
}

宏的重复语法:$(模式) 分隔符 重复次数,这里得分隔符是 重复单元分隔,是可选的
其中重复次数:

  • *:重复0次或多次
  • +:重复1次或多次
  • ?:出现0或1次

1.2.2 多规则模式

多规则模式(类似 match):宏可以写多条规则,按顺序匹配,思想像 match 而不是正则

macro_rules! calc {
 (add $a:expr, $b:expr) => { $a + $b }; //规则1:字面 token add +两表达式 
 (mul $a:expr, $b:expr) => { $a * $b }; //规则2
 }

let x = calc!(add1,2); //展开为1 +2let y = calc!(mul3,4); //展开为3 *4

这里的 add 是自定义的字面 token,宏模式里不带 $前缀的标识符就是字面 token,多规则之间互相区分的唯一手段,调用宏时必须原样写出这个词,才能匹配上这条规则:calc!(add ...)

如果去掉了字面token 后,mul那条规则就永远失效

macro_rules! calc {
 ($name:expr) => { println!("Hello, {}!", $name) };
 ($a:expr, $b:expr) => { $a + $b }; //规则2 
 ($c:expr, $d:expr) => { $c * $d }; //规则3:死代码!
 }

calc!(3,4); //想让它做乘法?没门 →结果永远是7(加法)

因为宏的规则匹配是从上往下,第一个匹配上的就生效,后面不再尝试。规则2和规则3的模式一模一样(都是"表达式,表达式"),任何两个表达式的调用都会被规则2拦截,规则3这辈子都轮不到

字面token的真正作用:让调用方能选路

macro_rules! calc {
 ($name:expr) => { println!("Hello, {}!", $name) };
 (add $a:expr, $b:expr) => { $a + $b };
 (mul $a:expr, $b:expr) => { $a * $b };
}
calc!(1); //走规则1:Hello,1!
calc!(add 3,4); //走规则2:7
calc!(mul 3,4); //走规则3:12 ←靠 mul这个词区分出来

1.2.3 例子分析

1.2.3.1 简单例子

下面是一个简单的宏定义的例子:

// 宏的定义
macro_rules! greet {    
    // 模式匹配
    ($name:expr) => { 
        // 宏的展开
        println!("Hello, {}!", $name);
    };
}
fn main() {
    // 调用宏
    greet!("World");
}

说明

  • 模式匹配:宏通过模式匹配来匹配传递给宏的代码片段,模式是宏规则的左侧部分,用于捕获不同的代码结构。
  • 规则:宏规则是一组由 $ 引导的模式和相应的展开代码,规则由分号分隔。
  • 宏的展开:当宏被调用时,匹配的模式将被替换为相应的展开代码,展开代码是宏规则的右侧部分。

1.2.3.2 宏重复示例

下面是一个更复杂的例子,演示了如何使用宏创建一个简单的 vec! 宏,以便更方便地创建 Vec:

// 宏的定义
macro_rules! vec {
    // 基本情况,空的情况
    () => {
        Vec::new()
    };
    // 一次性的重复展开 
    // 表达式用逗号分隔 +表示至少1次  尾逗号:?表示0或1次
    ($($element:expr),+ $(,)?) => {
        {
            let mut temp_vec = Vec::new();
            $(
                temp_vec.push($element);
            )+
            temp_vec
        }
    };
}

fn main() {
    // 调用宏
    let my_vec = vec![1, 2, 3];
    println!("{:?}", my_vec); // 输出: [1, 2, 3]

    let empty_vec:Vec<i32> = vec![];
    println!("{:?}", empty_vec); // 输出: []
}

在这个例子中,vec! 宏使用了模式匹配,以及 $($element:expr),+ $(,)? 这样的宏重复语法来捕获传递给宏的元素,并用它们创建一个 Vec。$(,)? 用于处理末尾的逗号,也是宏重复语法

1.2.4 输出宏 println!

println! 格式输出的命令:print!("hello,{}",a),可以在 {} 之间可以放一个数字,它将把之后的可变参数当作一个数组来访问,下标从 0 开始:println!("a is {0}, a again is {0}", a);
格式字符串中通过 {{}} 分别转义代表 {}。但其他常用转义字符与 C 语言里的转义字符一样,都是反斜杠 \ 开头的形式

  • 基础输出
    {}:标准输出(Display 格式)
    {:p}:用于输出内存地址(指针)
    {:?}:用于调试,会以开发者友好的方式打印出数据结构内部的值。要使用它,自定义类型(如结构体)必须实现 Debug trait(通常通过在结构体上方加 #[derive(Debug)] 来自动实现)
    {:#?}{:?} 一样用于调试,但会自动换行和缩进,打印复杂结构体或数组时非常清晰易读。
  • 进制转换(数字专属),如果加上 # 会带上进制前缀,比如 {:#b} 会输出 0b1010
    {:b}:二进制(如 1010)
    {:o}:八进制(如 12)
    {:x}:小写十六进制(如 a)
    {:X}:大写十六进制(如 A)
  • 对齐与填充(常用于打印表格)
    {:<10}:左对齐,总宽度占 10 个字符,不足补空格。
    {:>10}:右对齐,总宽度占 10 个字符。
    {:^10}:居中对齐。
    {:0>5}:右对齐,不足 5 位用数字 0 填充(比如 42 会变成 00042)。
  • 精度控制
    {:.2}:浮点数保留 2 位小数(如 3.14)。
    {:.3}:如果是字符串,则只截取前 3 个字符。

1.3 过程宏

1.3.1 简介

过程宏是一种更为灵活和强大的宏,允许在编译时通过自定义代码生成过程来操作抽象语法树(AST)。过程宏在功能上更接近于函数,但是它们在编写和使用上更加复杂。

过程宏的类型:

类型 用法形态 作用
派生宏(Derive) #[derive(MyTrait)] 自动为结构体实现 trait
属性宏(Attribute) #[my_attr] 改造/包装被标注的代码
函数式宏(Function-like) my_macro!(...) 长得像声明宏,但内部是代码生成

这里需要在项目的 Cargo.toml 添加如下:

[lib]
proc-macro = true

过程宏入参 input: TokenStream 是一段原始 token流(类似未解析的字符串),无法直接用,需要 parse_macro_input! 把它解析成结构化的 DeriveInput,才能使用

DeriveInput 结构体有几个核心字段:

字段 含义 示例
ident 类型名称标识符 User
data 内容(是 struct/enum/union) Data::Struct(...)
attrs 属性列表 #[derive(...)]
generics 泛型参数 <T>

1.3.2 派生宏

派生宏(Derive Macro)是过程宏中最常见的一种,它允许通过 #[derive(MyTrait)] 属性自动为结构体或枚举实现特定的 trait。Rust 标准库中的 DebugClonePartialEq 等 trait 就是通过派生宏实现的。自定义派生宏可以极大地减少重复代码,提升开发效率。

1.3.2.1 定义

  • 定义位置:派生宏必须在独立的 crate 中定义,且该 crateCargo.toml 中需声明 [lib] proc-macro = true
  • 函数签名:派生宏的函数接收一个 TokenStream 作为输入(即 #[derive(...)] 所标注的代码),并返回一个新的 TokenStream(即生成的代码)。
  • 关键库:syn用于将 TokenStream 解析为结构化的 Rust 语法树(AST)而 quote是用于将 Rust 代码模板转换回 TokenStream,支持变量插值(#variable)。

1.3.2.2 简单例子分析

我们将创建一个派生宏 HelloDebug,它不仅实现 Debug trait,还会在输出中包含结构体名称和每个字段的名称与值。
定义宏的 Crate (hello_macro),首先,创建一个新的库 crate:

cargo new hello_macro --lib

编辑 hello_macro/Cargo.toml

[lib]
proc-macro = true

[dependencies]
syn = "2.0"
quote = "1.0"

2. 实现派生宏 (hello_macro/src/lib.rs)

use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput, Data};

#[proc_macro_derive(HelloDebug)]
pub fn hello_debug_derive(input: TokenStream) -> TokenStream {
    let ast: DeriveInput = parse_macro_input!(input);// 1. 解析输入为 DeriveInput 结构体
    // 2. 提取结构体/枚举的名称
    let name = &ast.ident;
    // 3. 根据数据类型(结构体、枚举、联合体)生成不同的匹配代码
    let debug_impl = match ast.data {
        Data::Struct(ref data_struct) => {
            // 处理具名字段的结构体 (struct Point { x: i32, y: i32 })
            if let syn::Fields::Named(ref fields) = data_struct.fields {
                let field_idents: Vec<_> = fields.named.iter().map(|f| &f.ident).collect();
                let field_names: Vec<String> = field_idents.iter().map(|ident| ident.as_ref().unwrap().to_string()).collect();
                
                quote! {
                    impl std::fmt::Debug for #name {
                        fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
                            // 动态生成字段输出
                            let mut debug_struct = f.debug_struct(stringify!(#name));
                            #(
                                debug_struct.field(#field_names, &self.#field_idents);
                            )*
                            debug_struct.finish()
                        }
                    }
                }
            } else {
                // 处理元组结构体或无字段结构体(简化处理)
                quote! {
                    impl std::fmt::Debug for #name {
                        fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
                            write!(f, "HelloDebug {{ type: {} }}", stringify!(#name))
                        }
                    }
                }
            }
        }
        Data::Enum(_) | Data::Union(_) => {
            // 对于枚举和联合体,提供一个基础实现(可根据需要扩展)
            quote! {
                impl std::fmt::Debug for #name {
                    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
                        write!(f, "HelloDebug(enum/union): {}", stringify!(#name))
                    }
                }
            }
        }
    };

    // 4. 将生成的代码转换回 TokenStream 并返回
    TokenStream::from(debug_impl)
}

代码解析:

  • parse_macro_input!:将原始的 TokenStream 解析为 DeriveInput,方便我们访问结构体的名称、字段等信息。
  • ast.ident:获取被标注的类型名称(例如 Point)。
  • quote! 宏:用于编写代码模板。在模板中,#name 会被替换为实际的类型名,#(...)* 是重复展开语法,这里是模仿重复宏语法是 quote! 特有的重复语法,用于为每个字段生成对应的 debug_struct.field(...) 调用。
  • 匹配 ast.data:根据类型是结构体、枚举还是联合体,生成不同的 Debug 实现。这里重点展示了如何处理具名字段的结构体。

3. 使用派生宏

在另一个项目(例如 main crate)中,添加依赖并使用:

[dependencies]
hello_macro = { path = "../hello_macro" }
use hello_macro::HelloDebug;

#[derive(HelloDebug)]
struct Point {
    x: i32,
    y: i32,
}

#[derive(HelloDebug)]
struct User {
    id: u64,
    name: String,
    active: bool,
}

fn main() {
    let p = Point { x: 10, y: 20 };
    let u = User { id: 1, name: "Alice".to_string(), active: true };

    println!("{:?}", p); // 输出: Point { x: 10, y: 20 }
    println!("{:?}", u); // 输出: User { id: 1, name: "Alice", active: true }
}

1.3.2.3 复杂例子:生成 Builder 模式

派生宏还可以用于自动生成构建器(Builder)模式代码,这是一个非常实用的场景。

// 在 hello_macro crate 中定义另一个派生宏
#[proc_macro_derive(Builder)]
pub fn builder_derive(input: TokenStream) -> TokenStream {
    let ast = parse_macro_input!(input as DeriveInput);
    let name = &ast.ident;
    let builder_name = syn::Ident::new(&format!("{}Builder", name), name.span());
    // 假设只处理具名字段的结构体
    let fields = if let syn::Data::Struct(syn::DataStruct { fields: syn::Fields::Named(ref fields), .. }) = ast.data {
        &fields.named
    } else {
        panic!("Builder derive only supports structs with named fields");
    };
    // 为每个字段生成 setter 方法
    let setter_methods = fields.iter().map(|f| {
        let field_name = &f.ident;
        let field_type = &f.ty;
        quote! {
            pub fn #field_name(mut self, value: #field_type) -> Self {
                self.#field_name = Some(value);
                self
            }
        }
    });

    // 生成 Builder 结构体的字段(每个都是 Option)
    let builder_fields = fields.iter().map(|f| {
        let field_name = &f.ident;
        let field_type = &f.ty;
        quote! {
            #field_name: std::option::Option<#field_type>
        }
    });

    // 生成 build 方法,用于最终构建目标结构体
    let build_assignments = fields.iter().map(|f| {
        let field_name = &f.ident;
        quote! {
            #field_name: self.#field_name.ok_or(format!("field `{}` not set", stringify!(#field_name)))?
        }
    });

    let expanded = quote! {
        // 定义 Builder 结构体
        struct #builder_name {
            #(#builder_fields,)*
        }

        impl #builder_name {
            #(#setter_methods)*

            pub fn build(self) -> Result<#name, String> {
                Ok(#name {
                    #(#build_assignments,)*
                })
            }
        }

        // 为原结构体实现 builder() 方法
        impl #name {
            pub fn builder() -> #builder_name {
                #builder_name {
                    #(
                        #field_name: None,
                    )*
                }
            }
        }
    };

    TokenStream::from(expanded)
}

使用 Builder 派生宏:

#[derive(Builder)]
struct Config {
    timeout: u32,
    retries: u8,
    url: String,
}

fn main() {
    let config = Config::builder()
        .timeout(30)
        .retries(3)
        .url("https://example.com".to_string())
        .build()
        .unwrap();
    println!("Config built: {:?}", config);
}

1.3.3 属性宏

// 定义
#[proc_macro_attribute]
pub fn route(attr: TokenStream, item: TokenStream) -> TokenStream {
    // attr: "/hello"
    // item: fn hello() { ... }
    let path = attr.to_string();
    let func = parse_macro_input!(item as syn::ItemFn); //func =整个函数的结构化表示
    //let func_name = &func.sig.ident;//只是函数名这一个标识符
    let route_fn_name = format_ident!("{}_route_path", func.sig.ident);
    let expanded = quote! {
        #func  // 保留原函数      
        // 额外生成注册代码  为了 让框架在运行时能找到路由路径:
        fn #route_fn_name () -> &'static str {
            #path
        }
    };
    TokenStream::from(expanded)
}

使用:

#[route("/hello")]
fn hello() -> &'static str {
    "Hello, World!"
}

1.3.4 函数式宏

use  syn::{parse_macro_input, LitStr};
// 定义
#[proc_macro]
pub fn sql(input: TokenStream) -> TokenStream {
    // 解析 SQL 字符串,生成类型安全的查询代码
    let lit = parse_macro_input!(input as LitStr); //要求输入是字符串字面量 
    let sql_str = lit.value(); //拿到纯文本,不带引号
    let expanded = quote! {
        // 生成的代码
        format!("执行 SQL: {}", #sql_str)
    };    
    TokenStream::from(expanded)
}

使用:

let query = sql!(SELECT * FROM users WHERE id = 1);
// 编译时解析 SQL,生成对应代码
posted @ 2026-08-01 14:08  上善若泪  阅读(8)  评论(0)    收藏  举报