Macro(宏) 到底是个什么东西,在日常生活中很难找到对应的概念。这里了不解释Macro的含义了,感兴趣的可以自行Google。下面先看一个C语言的例子:
#include <stdio.h>
#define PI 3.14
#define MAX(a, b) (((a) > (b)) ? (a): (b))
int main(void){
printf("max: %f\n", MAX(PI, 3.13));
return 0;
}
其中的 #define
语句叫做宏定义。
通过gcc -E srcfile.c 宏展开后的代码:
#include <stdio.h>
int main(void){
printf("max: %f\n", (((3.14) > (3.13)) ? (3.14) : (3.13)));
return 0;
}
在这里使用gcc的编译预处理命令,进行宏展开。对比前后的代码能明显发现不同点,之后的代码的PI
与 MAX
被替换了。替换的过程:
MAX(PI, 3.13) -> (((PI) > (3.13)) ? (PI) : (3.13))) -> (((3.14) > (3.13)) ? (3.14) : (3.13)))
整个宏的核心就是预处理阶段的替换操作。下面看看Rust语言Macro的一些用法,相对于C语言的Macro,Rust的Macro功能更加强大,但核心还是预处理阶段的替换操作。
Rust实例1:
macro_rules! a_macro {
() => {
println!("This is a_macro");
};
}
macro_rules! print_ex {
($e:expr) => {
println!("{:?} = {:?}", stringify!($e), $e);
};
}
fn main() {
a_macro!();
print_ex!({
let y = 20;
let z = 30;
z + y + 10 + 3 * 100
});
}
输出:
This is a_macro
"{ let y = 20; let z = 30; z + y + 10 + 3 * 100 }" = 360
上面两个宏分别一个不带参数,一个带参数的。它们是宏的一般用法。
Rust实例2:
macro_rules! x_and_y {
(x => $e:expr) => {
println!("x: {}", $e);
};
(y => $e:expr) => {
println!("y: {}", $e);
};
}
fn main() {
x_and_y!(x => 10);
x_and_y!(y => 20 + 30);
}
输出:
x: 10
y: 50
上面的宏是带条件分支的,简单来说就是宏根据参数的形式来选择调用哪个宏进行展开。就像上面的宏根据占位符x=>
和 y=>
的不同选择不同的宏分支进行展开。
Rust实例3:
macro_rules! build_fn {
($func_name: ident) => {
fn $func_name() {
println!("You Called {:?}()", stringify!($func_name));
}
};
}
fn main() {
build_fn!(hi_there);
hi_there();
}
输出:
You Called "hi_there"()
上面通过向宏传入函数名来生成函数,这个用法还是有点意思的。
Rust实例4:
macro_rules! compr {
($id1: ident | $id2: ident <- [$start: expr; $end: expr], $cond: expr) => {{
let mut vec = Vec::new();
for num in $start..$end + 1 {
if $cond(num) {
vec.push(num);
}
}
vec
}};
}
fn main() {
let evens = compr!(x | x <- [1;10], |x| x % 2 == 0);
println!("{:?}", evens);
let odds = compr![x | x <- [1;10], |x| x % 2 != 0];
println!("{:?}", odds);
}
输出:
[2, 4, 6, 8, 10]
[1, 3, 5, 7, 9]
上面的例子,简单的展示了使用宏自定义一些语法。
Rust实例5:
macro_rules! new_map {
($($key: expr => $val: expr),*) => {
{
use std::collections::HashMap;
let mut map = HashMap::new();
$(map.insert($key, $val);)*
map
}
};
}
fn main() {
let m = new_map! {
"one" => 1,
"tow" => 2,
"tree" => 3
};
println!("{:?}", m);
}
输出:
{"one": 1, "tow": 2, "tree": 3}
通过上面的例子了解可变参数的使用方法,其中*
的含义是匹配0个或多个参数。
Rust实例6:
macro_rules! calc {
(eval $e: expr) => {
{
let val: usize = $e;
println!("{} = {}", stringify!($e), val);
}
};
(eval $e: expr, $(eval $es: expr),+) => {
{
calc! {eval $e}
calc! { $(eval $es),+ }
}
};
}
fn main() {
calc! {
eval 4 * 5,
eval 1 + 10,
eval (10 * 3) - 20
}
}
输出:
4 * 5 = 20
1 + 10 = 11
(10 * 3) - 20 = 10
上面的例子通过宏递归展开,首先调用宏的第二个分支,消耗掉参数eval 4 * 5
;接着还是调用第二个分支,消耗掉参数eval 1 + 10
; 最后调用第一个分支,消耗掉eval (10 * 3) - 20
。值得一提的是 +
是匹配1个或过个参数,在递归宏的用法中 +
是很常用的。
Rust 实例7:
macro_rules! print_out_value {
() => {
println!("{}", out_value);
};
}
fn main() {
let out_value = 32;
print_out_value!();
}
输出:
error[E0425]: cannot find value `out_value` in this scope
--> d.rs:3:24
|
3 | println!("{}", out_value);
| ^^^^^^^^^ not found in this scope
...
9 | print_out_value!();
| ------------------- in this macro invocation
error: aborting due to previous error
Rust的宏是有自己的作用域,外部的变量必须通过参数的方式传递。不像C的宏,可以捕获外部变量(作用域规则)。我更喜欢Rust的设定,C中宏内外存在同名变量时,容易产生bug。
题外话
通过上面的几个例子,能对宏的用法有个大概了解。不禁会想,宏的部分场景也能用函数实现,的确是这样的。我认为除非函数调用成为性能瓶颈时(函数调用栈会消耗性能,宏是预处理阶段直接插入代码,没有函数调用栈),能用函数的地方就用函数。因为无论是C/C++还是Rust,宏出bug调试有点麻烦。代码可读性就不好说了,有的地方使用宏也不会带来代码可读性问题。但大多数情况下宏还是会降低代码可读性。