之前的文章:
本章主要介绍 rust 宏的基本用法,主要包括 macro_rules 的基本语法规则和各种细节语法规则。
宏的基础语法
宏是 rust 中基本的代码抽象机制,可以有效减少代码中重复的代码片段。rust 的宏分为两种:使用 macro_rules! 定义的普通宏( macro )和用法非常复杂的过程宏( procedural macro )。本章介绍较为易用的前者。
在编写代码时,有时会写出一些重复或近似重复的代码片段,这些代码片段有时还很难使用函数、 trait 或其他语法来进行抽象,此时可以考虑使用宏。例如:
struct WholeDayTemperature {morning: u32,noon: u32,evening: u32,midnight: u32,}impl WholeDayTemperature {fn print_all(&self) {println!("Temperature for morning is {}", self.morning);println!("Temperature for noon is {}", self.noon);println!("Temperature for evening is {}", self.evening);println!("Temperature for midnight is {}", self.midnight);}}fn main() {let wdt = WholeDayTemperature {morning: 30,noon: 36,evening: 34,midnight: 28,};wdt.print_all();}
上面这段代码中,对四个字段的输出是相似的代码逻辑,但是无法使用函数来进行抽象。此时可以用宏来进行抽象,例如:
struct WholeDayTemperature {morning: u32,noon: u32,evening: u32,midnight: u32,}impl WholeDayTemperature {fn print_all(&self) {// 定义一个宏,名字是 print_itemmacro_rules! print_item {// 这个宏接受一个参数,类型是变量名( ident )($v: ident) => {// 将变量名转换成一个字符串,如 my_var 会变成 "my_var"let v_str = stringify!($v);println!("Temperature for {} is {}", v_str, self.$v);}}// 调用宏,传入 morning 作为参数print_item!(morning);// 这个调用等价于直接编写:// let v_str = stringify(morning);// println!("Temperature for {} is {}", v_str, self.morning);// 进一步等价于:// println!("Temperature for {} is {}", "morning", self.morning);// 以其他参数调用同一个宏print_item!(noon);print_item!(evening);print_item!(midnight);}}fn main() {let wdt = WholeDayTemperature {morning: 30,noon: 36,evening: 34,midnight: 28,};wdt.print_all();}
元变量
在 macro_rules! 调用中,可以指定接受的“元变量”及参数类型。元变量即 $ 开头的变量,与普通变量不同,它的值可以是一个变量名 ident 、表达式 expr 或其他“代码片段”。元变量支持的类型列表可以在 rust 文档中找到:
https://doc.rust-lang.org/reference/macros-by-example.html#metavariables
使用元变量时,会将对应的变量名(或表达式等)替换到这个元变量出现的位置上,如:
macro_rules! print_item {($v: ident, $desc: expr) => {println!("Temperature {} is {}", $desc, self.$v);}}print_item!(morning, "in the morning");// 等价于println!("Temperature {} is {}", "in the morning", self.morning);
此外, macro_rules! 可以接受多种不同类型、不同数量的参数,用法类似于 match ,例如:
macro_rules! print_item {// 如果传入两个参数,且参数分别为 ident 和 expr ,则匹配这个分支($v: ident, $desc: expr) => {println!("Temperature {} is {}", $desc, self.$v);};// 如果只传入一个参数,且类型是 ident ,则匹配这个分支($v: ident) => {let v_str = stringify!($v);println!("Temperature for {} is {}", v_str, self.$v);};}print_item!(morning, "in the morning");// 等价于println!("Temperature {} is {}", "in the morning", self.morning);print_item!(morning);// 等价于let v_str = stringify!(morning);println!("Temperature for {} is {}", v_str, self.morning);
列表
macro_rules! 也支持传入无限个参数,将参数表达为一个列表,用法例如:
struct WholeDayTemperature {morning: u32,noon: u32,evening: u32,midnight: u32,}impl WholeDayTemperature {fn print_all(&self) {macro_rules! print_items {// 接受一个无限的参数列表 v (列表用 , 分隔)($($v: ident),*) => {// 展开列表,// 对于列表中的每一项重复下面括号中的内容$(let v_str = stringify!($v);println!("Temperature for {} is {}", v_str, self.$v);)*};}print_items!(morning, noon, evening, midnight);}}fn main() {let wdt = WholeDayTemperature {morning: 30,noon: 36,evening: 34,midnight: 28,};wdt.print_all();}
利用列表特性可以将代码变得更加简洁。上面这段代码与下例等价:
macro_rules! whole_day_temperature {($($v: ident),*) => {struct WholeDayTemperature {$($v: u32,)*}impl WholeDayTemperature {fn print_all(&self) {$(let v_str = stringify!($v);println!("Temperature for {} is {}", v_str, self.$v);)*}}};}whole_day_temperature!(morning, noon, evening, midnight);fn main() {let wdt = WholeDayTemperature {morning: 30,noon: 36,evening: 34,midnight: 28,};wdt.print_all();}
此外,调用宏时,除了 !(...) 的形式,也可以用 ![...] 或者 !{...} 的形式,有时会更好看,例如:
whole_day_temperature! { morning, noon, evening, midnight }
宏的作用域
宏可以在 macro_rules! 代码块之后的子模块中直接访问。
mod my_mod {mod my_sub_mod_a {// 编译错误!// macro_rules 定义之前不能调用my_macro!();}macro_rules! my_macro {() => { /* ... */ }}mod my_sub_mod_b {// 子模块中可以直接调用my_macro!();}}
如果需要在父模块或者其他 crate 中使用这个宏,需要加上 #[macro_export] 。
mod my_mod {#[macro_export]macro_rules! my_macro {() => { /* ... */ }}}// macro_export 使其可以在模块外调用my_macro!();
在其他 crate 中使用这个 crate 定义的宏时,需要加上 #[macro_use] 。例如:
#[macro_use(my_macro)]extern crate my_crate;
变量名拼接
需要注意的是, macro_rules! 中无法进行变量名的拼接。如果需要在宏中拼接字符串,可以使用 paste 这个第三方 crate 。首先在 Cargo.toml 中引入:
[dependencies]paste = "1.0"
具体用法例如:
use paste::paste;macro_rules! whole_day_temperature {($($v: ident),*) => {struct WholeDayTemperature {$($v: u32,)*}impl WholeDayTemperature {fn print_all(&self) {$(let v_str = stringify!($v);println!("Temperature for {} is {}", v_str, self.$v);)*}// paste! 内支持变量名拼接paste! {$(// 拼接 set_ 和 $v ,生成一个 set_* 方法fn [<set_ $v>](&mut self, t: u32) {self.$v = t;})*}}};}whole_day_temperature!(morning, noon, evening, midnight);fn main() {let mut wdt = WholeDayTemperature {morning: 30,noon: 36,evening: 34,midnight: 28,};// 调用 set_* 方法wdt.set_morning(26);wdt.print_all();}
macro_rules! 是一种简易的宏定义方式。过程宏比它更加强大,但用法也复杂得多。之后的文章将详细介绍过程宏。




