rust学习二十.14、RUST宏
毫无疑问,宏是rust中极其重要的存在,只要我们看看一些标准库的代码。本文阐述rust宏相关的若干问题
一、宏的定义和作用
先介绍下宏的起源和历史,以便读者更能体会。
以下内容来自文心一言生成:
词源与早期含义
- 希腊-拉丁词根
- "macro"源于希腊语前缀μακρο-(makro-),意为“大”或“长”。拉丁语继承为macro-(如macro-instruction),保留“大”的核心含义。
- 在英语中,"macro"作为形容词表示“巨大的”(如macroeconomic),作为名词则特指计算机领域的“宏指令”。
- 早期通用含义
- 在非技术领域,"macro"常用于描述大规模事物(如macro-scale planning),与“微观”(micro)形成尺度对比。
计算机领域的起源与演变
- 20世纪50年代:汇编语言的宏指令
- 背景:早期计算机编程依赖汇编语言,重复性任务(如寄存器操作)导致代码冗长。
- 突破:IBM 705计算机(为陶氏化学公司开发)首次引入宏指令(macro-instruction),允许用简短符号代替复杂指令序列。
- 1960年代:LISP语言的元编程宏
- 关键人物:Timothy Hart在1963年提出在LISP 1.5中添加宏,通过宏编译器将高阶语法转换为底层代码。
- 意义:宏成为编程语言扩展机制,支持用户自定义语法(如定义领域专用语言DSL)。
- 1990年代:VBA与办公自动化
- 转折点:微软在Office套件中集成VBA(Visual Basic for Applications),使宏进入大众视野。
- 应用爆发:用户可通过录制宏自动化Excel公式计算、Word文档排版等操作,显著提升办公效率。
基本没有什么毛病!
概括下:
1.macro(宏)-起源于希腊,后拉丁借用,最后是英语用上了。
2.macro的核心意思就是:巨大,宏观(大规模现象之意)
3.在计算机领域,大体保留了对macro的含义,例如宏病毒,宏指令
具体到计算机,可以这样理解:宏或者宏指令就是表达了一大段代码的对象(命令/指令等),表达了有许多代码的意思。
这是一个习惯问题。只要愿意,我们也可以称为:多指令集合,多代码集合。
所以,总结起来,rust宏定义:用于表示一段代码或者条件语句,其构成可能是一段代码,或则可能也没有任何代码。
或者说宏的核心作用就是两个
1.条件编译
2.表示一段代码,有助于简化编程
从这里可以看出rust宏和C++宏的作用极其相似!
二、宏实现原理
从其自用可以看出,其实现原理相当简单:
1.如果是条件编译指令,那么编译器发现后当做条件执行
2.如果表示一段代码,则rust编译器会把指令替换为一段代码(大体是提单,但具体上应该还是有一些额外的内容)
宏都是在编译的时候起到作用!
三、宏和函数区别
书籍作者为什么要提到这个? 开始的时候我不太明白,直到我看了宏的代码。
宏的代码看起来有点像函数。
这里先总结下rust中宏和函数的区别:
1.宏可不仅仅是一段代码,还可以作为编译条件
2.宏可以接收不同个数参数(或者说不定个数参数),而函数不能
3.宏是在编译环节起作用(编译器会根据宏类型来决定作为编译条件还是实现一段代码),而函数是在运行时起到作用
四、标准宏
4.1、常见标准宏
表_常见标准宏
宏名称 | 功能描述 | 所属模块/类型 |
---|---|---|
assert! |
断言表达式为 true ,否则触发 panic(常用于单元测试) |
标准库(测试相关) |
assert_eq! |
断言两个值相等,否则触发 panic | 标准库(测试相关) |
assert_ne! |
断言两个值不相等,否则触发 panic | 标准库(测试相关) |
compile_error! |
在编译期生成错误(用于自定义编译时检查) | 标准库(元编程) |
concat! |
连接多个字面量,返回 &'static str |
标准库(字符串操作) |
column! |
返回当前源代码的列号 | 标准库(调试信息) |
dbg! |
输出表达式的值和类型(调试工具) | 标准库(调试相关) |
eprint! |
输出到标准错误流(不换行) | 标准库(I/O 操作) |
eprintln! |
输出到标准错误流(自动换行) | 标准库(I/O 操作) |
env! |
获取编译期环境变量(若变量不存在则报错) | 标准库(环境变量) |
file! |
返回当前源代码的文件名 | 标准库(调试信息) |
format! |
格式化字符串并返回 String |
标准库(字符串操作) |
include_bytes! |
将文件读取为字节数组(&'static [u8] ) |
标准库(文件操作) |
include_str! |
将文件读取为字符串(&'static str ) |
标准库(文件操作) |
line! |
返回当前源代码的行号 | 标准库(调试信息) |
macro_rules! |
定义声明式宏(如 vec! ) |
标准库(宏定义) |
module_path! |
返回当前模块的路径 | 标准库(调试信息) |
option_env! |
安全获取编译期环境变量(返回 Option<&'static str> ) |
标准库(环境变量) |
print! |
输出到标准输出流(不换行) | 标准库(I/O 操作) |
println! |
输出到标准输出流(自动换行) | 标准库(I/O 操作) |
stringify! |
将类型或标记转换为字符串字面量 | 标准库(类型操作) |
vec! |
创建并初始化向量(如 vec![1, 2, 3] ) |
标准库(集合操作) |
以上这些宏是截止2025/04/11,这个是的rust主版本是2021
4.2、常见编译宏
表_rust常见编译宏
宏名称 | 功能描述 | 所属模块/类型 |
---|---|---|
cfg! |
在编译时评估配置标志的布尔组合(如 if cfg!(target_os = "linux") ) |
标准库(条件编译) |
debug_assertions |
检查是否启用了调试断言(未启用优化时成立) | 标准库(调试配置) |
target_arch |
匹配目标平台的CPU架构(如 x86_64 、arm ) |
标准库(平台配置) |
target_env |
匹配目标平台的运行库环境(如 musl 、msvc ) |
标准库(平台配置) |
target_os |
匹配目标操作系统(如 linux 、windows ) |
标准库(平台配置) |
target_pointer_width |
匹配目标平台的指针宽度(如 32 、64 ) |
标准库(平台配置) |
target_vendor |
匹配目标平台的生产商(如 apple 、pc ) |
标准库(平台配置) |
test |
标记测试函数(仅在测试模式下编译) | 标准库(测试配置) |
表_rust条件编译控制宏等
名称 | 功能描述 | 示例 | 大类 |
---|---|---|---|
compile_note! | 输出编译提示信息(不中断编译) | compile_note!("此功能需要Rust 1.60+版本"); | 编译诊断宏 |
compile_warning! | 输出编译警告(不中断编译) | compile_warning!("使用旧版API可能影响性能"); | 编译诊断宏 |
compile_error! | 立即终止编译并报错(展开宏后停止) | compile_error!("不支持Windows XP系统"); | 编译诊断宏 |
compile_fatal! | 立即终止编译(最高优先级错误) | compile_fatal!("致命错误:缺少必要配置文件"); | 编译诊断宏 |
allow | 忽略特定警告 | #![allow(dead_code)] | Lint控制属性 |
warn | 将特定警告提升为错误(编译失败) | #![warn(missing_docs)] | Lint控制属性 |
deny | 将特定警告提升为错误(编译失败) | #![deny(unused_variables)] | Lint控制属性 |
forbid | 禁止特定代码模式(最高优先级,编译失败) | #![forbid(unsafe_code)] | Lint控制属性 |
deprecated | 标记函数/结构体为已弃用(生成警告) | // 示例:在函数上添加#[deprecated]属性 | 其他相关宏/属性 |
must_use | 强制检查返回值是否被使用(如Result 类型) |
// 示例:在结构体上添加#[must_use]属性 | 其他相关宏/属性 |
cfg/cfg_attr | 根据编译条件启用不同属性 | // 示例:#[cfg(target_os = "windows")] #[allow(unused_variables)] | 其他相关宏/属性 |
五、自定义宏的编写
光有标准宏也不够,rust还允许工程师自定义宏,就和C++一样。
在rust中可以通过以下两种方式创建自定义宏
1.使用macro_rules!的申明宏
2.过程宏
- 自定义
#[derive]
宏,用于在结构体和枚举上通过添加derive
属性生成代码 - 类属性宏,定义可用于任意项的自定义属性
- 类函数宏,看起来像函数,但操作的是作为其参数传递的 token
5.1、申明宏定义
这是比较常见的定义方式,关键在于知道怎么定义,或者说如何看懂宏定义。
虽然在实际编程中宏的定义不是很多,但是要知道如何看,要在需要定义的时候能够定义。
我们都知道宏太多的后遗症,C++就是这个毛病,C++也在努力知错就改!
闲话少说,以书本的例子进行解释:
#[macro_export]
macro_rules! vec {
( $( $x:expr ),* ) => {
{
let mut temp_vec = Vec::new();
$(
temp_vec.push($x);
)*
temp_vec
}
};
}
逐个解释下:
1.#[macro_export]-告诉编译器当前的宏可以在其它单元包中使用
2. macro_rules! 申明宏的固定开头
3.vec宏名称,注意不用带!
4.宏名vec后的括号{}及其内容是宏主体,包括一个模式匹配表达式(大体是一个正则表达式)和一个生成段(利用匹配表达式中捕获的变量)
其中2,3部分是固定。
上例中的模式匹配表达式
a.第一个$表示这是一个重复的匹配分组,和正则表达式匹配分组一个概念
b.$x:expr- $x表示捕获的变量,expr用于修饰$x表示这是一个表达式
c.最后的逗号表示重复分组的分隔符
d.最后的*表示这个重复分组可以0次或者多次,和一般的正则表达式的*一个意思。
需要注意的是:这个模式匹配表达式并不是标准的正则表达式语法。
它既不是基于NFA(非确定性有限自动机)的实现(例如perl,python) ,也不是基于DFA(确定性有限自动机)的实现(例如java,python),而是rust自己改造过的。
为了便于记忆,也可以成为rust宏模式匹配语法,或者rust宏模式匹配正则语法。
注意:一个宏定义中,可以有多个模式匹配表达式,以及每个匹配表达式对应的生成段。 上面这个官方的例子这是给出了最简单的示例-只是匹配了一种情况。
本文的重点不在于说明如何编写各种各样宏的具体细节,重点在于告知宏的主要方面。
如果要知道如何编制宏,可以参考官方给出的文档 - https://veykril.github.io/tlborm/
5.2、过程宏定义
过程宏是什么意思?
原文是procedural ,中文的意思可以是:程序性的,程序上的。procedural是procedure的形容词。
在许多语言中,procedure和function基本是一个意思。所以这里的过程宏意思就是和函数/方法有关的宏?
以下是原书文字:
过程宏接收 Rust 代码作为输入,在这些代码上进行操作,然后产生另一些代码作为输出,而非像声明式宏那样匹配对应模式然后以另一部分代码替换当前代码。
创建过程宏时,其定义必须驻留在它们自己的具有特殊 crate 类型的 crate 中。这么做出于复杂的技术原因,将来我们希望能够消除这些限制。
以下简要介绍三种过程中的定义,更多的直接看后面例子。
注:实际的定义可能更加复杂多样,以下内容仅供参考!
更多内容参考: https://veykril.github.io/tlborm/
或者直接阅读rust的源码:https://github.com/rust-lang/rust
5.2.1、继承宏derive定义
继承宏主要用于实现某个特质。定义特质的继承宏的主要目标是为了少写代码。
如果不习惯使用继承宏也无所谓。
当然定义一个继承宏,除了可以少写代码,也可以通过这个方式为相关的对象快速地添加其它的功能。
常见的如:Debug
、Clone
、PartialEq
它的定义形如:
5.2.2、类属性宏定义
类属性宏,就外在表现而言,在某种程度上和继承宏差不多。类属性宏可以用于函数/方法、struct,枚举(enum)、联合体(union)、类型别名(type alias)以及模块(mod)等所有"项"(Item)类型。
常见的类属性宏有cfg、test
类属性宏通常传递两个参数,例如:
5.2.3、类函数宏定义
类函数宏如起其名,在使用的时候,它表现得和一个函数一样,例如常见的vec!
、println!
、format!
它的定义比较简单,形如:
六、示例
内容有点多。
6.1、申明宏示例
/**
* 1.申明式宏例子
* 申明式宏以macro_rules! 关键字定义,用于在编译时进行代码的替换和生成。
* 后面跟上宏名称。内容可以包含多个模式匹配表达式,和表达式相对应的替换代码(或者称为生成段)
* 不同的匹配表达式之间使用分号进行分割.
* 而模式匹配表达式式一种类似于正则表达式的rust定义规则表达式
*/
macro_rules! example {
// 1. 简单固定匹配,不需要 $
() => {
println!("Empty match!");
};
// 2. 固定字面量匹配,不需要 $
(hello) => {
println!("Hello match!");
};
// 3. 使用元变量的匹配,需要 $
($x:expr) => {
println!("Expression: {}", $x);
};
// 4. 混合使用
(print $x:expr) => {
println!("Printing: {}", $x);
};
// 5. 重复模式,需要 $
(sum $($x:expr),*) => {
{
let mut total = 0;
$(
total += $x;
)*
total
}
};
}
fn main() {
example!(); // 匹配第1个规则
example!(hello); // 匹配第2个规则
example!(42); // 匹配第3个规则
example!(print 42); // 匹配第4个规则
let sum = example!(sum 1, 2, 3); // 匹配第5个规则
println!("Sum: {}", sum);
}
可以看出,申明宏很好用,用起来类似函数宏。
输出:
6.2、过程宏示例
包括三个宏:继承宏、类属性宏、类函数宏
为了方便,它们写在一个rust空间中,结构图如下:
wsmain是二进制单元主包,attr_macro,func_macro,hello_macro_derive分别表示类属性宏、类函数宏、继承宏。
这三个宏都是库单元。
各个Cargo.toml的文件
//1.wsexample的空间描述文件
[workspace]
resolver = "2"
members = [ "hello_macro_derive",
"student",
"teacher",
"hello_macro_derive",
"wsmain", "attr_macro", "func_macro",
]
[workspace.package]
version = "0.1.0"
edition = "2021"
[workspace.dependencies]
rand = "0.9.0-beta.1"
//2.wsmain工程描述文件
[package]
name = "wsmain"
version.workspace=true
edition.workspace=true
[dependencies]
student={path="../student"}
teacher={path="../teacher"}
hello_macro_derive={path="../hello_macro_derive"}
attr_macro={path="../attr_macro"}
func_macro={path="../func_macro"}
//3.attr_macro描述文件
[package]
name = "attr_macro"
version.workspace = true
edition.workspace = true
[lib]
proc-macro = true
[dependencies]
proc-macro2 = "1.0"
quote = "1.0"
syn = { version = "2.0", features = ["full"] }
//4.func_macro描述文件
[package]
name = "func_macro"
version.workspace = true
edition.workspace = true
[lib]
proc-macro = true
[dependencies]
syn = { version = "2.0", features = ["full"] }
quote = "1.0"
//5.hello_macro_derive描述文件
[package]
name = "hello_macro_derive"
version.workspace = true
edition.workspace = true
[lib]
proc-macro = true
[dependencies]
syn = "2.0"
quote = "1.0"
attr_macro库文件
//引入TokenStream, TokenTree, quote, syn等宏和库
//其中TokenStream的作用是生成和解析Token流,用于宏的定义和处理。
use proc_macro::{TokenStream, TokenTree};
use quote::quote;
//引入parse_macro_input, ItemFn等宏和库
//其中parse_macro_input用于解析宏输入,fnItemFn表示一个函数项,LitStr表示字符串字面量。
use syn::{parse_macro_input, ItemFn};
//基于这个类属性宏,那么函数可以这样引用 #[route(GET,"/")]
#[proc_macro_attribute]
pub fn route(attr: TokenStream, fn_item: TokenStream) -> TokenStream {
// 解析属性参数
let mut method = String::new();
let mut path = String::new();
for token in attr.clone() {
match token {
TokenTree::Literal(lit) => {
// 假设第二个参数是路径
path = lit.to_string().trim_matches('"').to_string();
}
TokenTree::Ident(ident) => {
// 假设第一个参数是HTTP方法
method = ident.to_string();
}
_ => {}
}
}
// 解析原始函数
let input_fn = parse_macro_input!(fn_item as ItemFn);
let fn_name = &input_fn.sig.ident;
let fn_block = &input_fn.block;
let fn_vis = &input_fn.vis;
let fn_sig = &input_fn.sig;
// 生成新的代码
let expanded = quote! {
#fn_vis #fn_sig {
println!(
"Route registered - Method: {}, Path: {}, Function: {}",
#method, #path, stringify!(#fn_name)
);
#fn_block
}
};
TokenStream::from(expanded)
}
这个宏实现route(GET,"/")这样的注解。
注意,例子中写死了是用于函数上,如果用于其它项目,可能会报错。我们也可以继续修改,以让它适用于更多的rust对象:结构、枚举、别名、模块等。
func_macro库文件
// 引入必要的过程宏库
use proc_macro::TokenStream;
use quote::quote;
use proc_macro::TokenTree;
// 定义函数宏
#[proc_macro]
pub fn array(input: TokenStream) -> TokenStream {
// 打印整个 input 的内容
//println!("Macro input (raw): {:?}", input);
// 打印 input 中的每个 token
println!("工程师在代码中输入的内容如下:");
for (index, token) in input.clone().into_iter().enumerate() {
//如果token是Literal,那么打印token.symbol
//如果token是Punct,那么打印token.ch
match token {
TokenTree::Literal(lit) => println!("Token {} - Literal: {}",index, lit.to_string()),
TokenTree::Punct(punct) => println!("Token {} - Punctuation: '{}'",index, punct.as_char()),
_ => (),
}
}
// 将 input 转换为迭代器
let mut iter = input.into_iter();
// 第一个参数:待分割的字符串
let content = loop {
match iter.next() {
Some(TokenTree::Literal(lit)) => {
// 找到字面量后返回
let content_str = lit.to_string().trim_matches('"').to_string();
println!("Parsed content: {}", content_str);
break content_str;
},
Some(TokenTree::Punct(_)) => {
// 遇到标点符号就继续循环
continue;
},
None => panic!("未找到字符串参数"),
_ => panic!("第一个参数必须是字符串"),
}
};
// 第二个参数:分隔符
let delimiter = loop {
match iter.next() {
Some(TokenTree::Literal(lit)) => {
// 找到字面量后返回
let delimiter_str = lit.to_string().trim_matches('"').to_string();
println!("Parsed delimiter: {}", delimiter_str);
break delimiter_str;
},
Some(TokenTree::Punct(_)) => {
// 遇到标点符号就继续循环
continue;
},
None => panic!("未找到分隔符参数"),
_ => panic!("第二个参数必须是分隔符字符串"),
}
};
// 分割字符串
let elements: Vec<String> = content
.split(&delimiter)
.map(|s| s.trim().to_string())
.collect();
// 打印分割后的元素
println!("Parsed elements: {:?}", elements);
// 使用 quote! 宏生成代码
let expanded = quote! {
[#(#elements),*]
};
// 转换为 TokenStream 并返回
TokenStream::from(expanded)
}
这个宏是为了实现这样的功能:把一个字符串切割为一个数组返回,分隔符可以自行定义。
例如 array!("bood,dsd,中国,美国,俄罗斯",",")。
注意,代码中执行了一些标准rust功能,这些输出只是在编译的时候产生,请仔细看最后的输出。
hello_macro_derive库文件
/**
* 演示一个过程宏的实现
* 其中关联文档
* 1. syn 的 DeriveInput 结构 -- https://docs.rs/syn/1.0.109/syn/struct.DeriveInput.html
* 2. quote crate 的quote! 宏 -- https://docs.rs/quote/latest/quote/
*/
use proc_macro::TokenStream;
use quote::quote;
/**
* 这是一个过程宏的典型实现,按照书本的阐述,大部分的过程宏的实现结构基本同此
* 1. 解析输入的代码
* 2. 基于解析的数据结构构建 trait 实现
* 下面的sysn::parse(input).unwrap() 就是第一步,它产生的结构大体如下:
* DeriveInput {
// --snip--
ident: Ident {
ident: "Pancakes",
span: #0 bytes(95..103)
},
data: Struct(
DataStruct {
struct_token: Struct,
fields: Unit,
semi_token: Some(
Semi
)
}
)
}
*/
#[proc_macro_derive(HelloMacro)]
pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
//1.使用syn crate 将字符串中的 Rust 代码解析成为一个可以操作的数据结构
let ast = syn::parse(input).unwrap();
//2.步骤2- Build the trait implementation
impl_hello_macro(&ast)
}
fn impl_hello_macro(ast: &syn::DeriveInput) -> TokenStream {
let name = &ast.ident;
// 使用quote!宏构建最终的trait实现
let gen = quote! {
impl Hello for #name {
fn hello_macro() {
println!("Hello, Macro! My name is {}!", stringify!(#name));
}
fn showme(&self)->String{
self.name.clone()+"[年龄:"+self.age.to_string().as_str()+"]"
}
fn get_name(&self)->String{
self.name.clone()
}
}
};
gen.into()
}
这宏就是为了给特质加功能,使得特定的对象具有特质的功能。
注意:代码中写死了对象必须具有name属性,这基本就意味着它只能用于结构体。
wsmain主体(main.rs)
use student::*;
use teacher::*;
use hello_macro_derive::HelloMacro;
use attr_macro::route;
use func_macro::array;
pub trait Hello{
fn hello_macro();
fn showme(&self)->String;
fn get_name(&self)->String;
}
#[derive(Debug, HelloMacro)]
struct Home{
name: String,
age: u32,
}
fn main() {
//1.测试其它单元包的使用情况
test_other_crate();
//2.测试盒子指针
let _me = Box::new(String::from("中国"));
let lu = Teacherinfo {
name: String::from("lu"),
age: 46,
gender: String::from("男"),
position: String::from("教研组长"),
};
let my_lu = MyTeacherinfo(lu);
my_lu.print();
//3.测试用户自定义的各种宏
println!("\n\n测试用户自定义的derive宏-----------------------");
// 宏helloMacro具有3个方法,分别是showme, get_name和hello_macro
test_udf_derive_macro();
println!("\n测试用户自定义的类属性宏-----------------------");
test_attr_macro();
println!("\n测试用户自定义的类函数宏-----------------------");
test_func_macro();
}
struct MyTeacherinfo(Teacherinfo);
impl Print for MyTeacherinfo {
fn print(&self) {
println!(
"教师基本信息-姓名:{},年龄:{},性别:{},职位:{}",
self.0.name, self.0.age, self.0.gender, self.0.position
);
}
}
/**
* 测试用户自定义的derive宏
*/
fn test_udf_derive_macro(){
let home = Home{name: String::from("home"), age: 18};
let me=home.showme();
println!("{}", me);
println!("{}", home.get_name());
Home::hello_macro();
}
#[route(GET,"/")]
fn test_attr_macro() {
}
fn test_func_macro() {
let arr = array!("1,2,3,4,5",",");
println!("{:?}",arr);
}
/**
* 测试对其它单元包的使用
*/
fn test_other_crate(){
let lml = Studentinfo {
name: String::from("lml"),
age: 18,
gender: String::from("女"),
no: String::from("12101"),
};
print_student(&lml);
lml.learn();
lml.sleep();
let lu = Teacherinfo {
name: String::from("lu"),
age: 46,
gender: String::from("男"),
position: String::from("教研组长"),
};
lu.teach_student(&lml);
print_teacher(&lu);
}
这个文件中主要是调用各种宏,也有其它一些内容.
输出结果:
可以看到在编译信息中有输出,这是attr_macro的编译输出,用于显示工程师录入的宏参数。
七、宏的调试技巧
和大部分的语言一言,宏的一个大毛病就是难于调试。
所以,要调试都是一些相对间接的方式,当然不排除以后工具或者方法有了改变,可以很方便。
- 用
cargo expand
获取展开代码 - 通过 IDE 查看宏展开预览 -- 仅限于预览,目前rustover有这个功能
- 在过程宏中添加日志输出 -- 例如前面的attr_macro就是这样的
- 编写针对宏的单元测试
- 使用
trace_macros!(true)
进行细粒度跟踪
expand的方式
cargo install cargo-expand
cargo expand --example your_example
使用trace_macros
// 在代码中添加跟踪标记
#[cfg(debug_assertions)]
macro_rules! trace_macros {
($($tt:tt)*) => {
println!("Macro expanded: {}!", stringify!($($tt)*));
};
}
// 或使用编译器内置跟踪
#[cfg(debug_assertions)]
#[macro_export]
macro_rules! dbg_macro {
($($tt:tt)*) => {
trace_macros!(true);
$($tt)*
};
}
之后build的时候记得添加标记,windows使用set RUSTFLAGS="-Z unstable-options --pretty=expanded"
八、小结
1.总体上rust宏是一个好东西,前提是谨慎使用,不要滥用,因为它不好调试,不容易优化
2.rust有两大类宏:申明宏和过程宏。过程宏包括继承宏、类属性宏、类函数宏
本文来自博客园,作者:正在战斗中,转载请注明原文链接:https://www.cnblogs.com/lzfhope/p/18821027