Rust笔记 - Macro

781 阅读3分钟

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的编译预处理命令,进行宏展开。对比前后的代码能明显发现不同点,之后的代码的PIMAX替换了。替换的过程:

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调试有点麻烦。代码可读性就不好说了,有的地方使用宏也不会带来代码可读性问题。但大多数情况下宏还是会降低代码可读性。