macro_rules

macro_rules是rust中定义宏的主要手段,例如 assert_eq!

  1. macro_rules! assert_eq {
  2. ($left:expr, $right:expr) => ({
  3. match (&$left, &$right) {
  4. (left_val, right_val) => {
  5. if !(*left_val == *right_val) {
  6. panic!("assertion failed");
  7. }
  8. }
  9. }
  10. });
  11. }

注意,这里assert_eq后是没有 ! 的, ! 只有在调用宏的时候才需要。

macro的函数体就是一系列的规则,类似match 例如

  1. (pattern1) => (template1);
  2. (pattern2) => (template2);

在调用宏的时候可以使用 {} [] () , 这些都是等价的,我们通常在assert_eq后用(), vec!后用[], macro_rules!后用{}, 这些都是些约定俗成的东西

  1. assert_eq!(gcd(6,10), 2);
  2. assert_eq![gcd(6,10), 2];
  3. assert_eq!{gcd(6,10), 2};

宏展开

当assert_eq! 调用时,rust首先模式匹配参数例如
image.png

rust的模式匹配类似正则匹配,与正则不同的是,rust的模式匹配作用于 tokens,这个例子中, $left:expr 告诉rust匹配一个表达式,并赋值$left, 然后匹配 , , 最后匹配第二个表达式,并赋值$right
image.png
先来看下这个宏,为什么这个宏要创建 left_val和right_val这两个变量?为什么不能直接写成 if $left != $right ? 加入当我们调用 assert_eq!(letters.pop, Some('z')) 的时候,所有的$left, $right都会被直接替换成这两个表达式,这里letters.pop会被多次调用,每次调用都会产生副作用,导致结果不正确,所以我们需要$left $right仅执行一次。

Repetition

再看看vec! 这个宏

  1. let buffer = vec![0_u8; 1000];
  2. let numbers = vec!["udon", "ramen", "soba"];

这个宏有3条rule

  1. macro_rules! vec{
  2. ($elem:expr ; &n:expr) => {
  3. ::std::vec::from_elem($elem, $n)
  4. };
  5. ( $( $x:expr ),* ) => {
  6. <[_]>::into_vec(Box::new([ $( $x ),* ]))
  7. };
  8. ( $( $x:expr ),+ ,) => {
  9. vec![ $( $x ),* ]
  10. };
  11. }

vec!["udon", "ramen", "soba"]; 匹配的是第二条, $( $x:expr ),* 这个表示匹配表达式 $x:expr, 0或多次,以逗号为分割符,更通用些表示为 $( PATTERN ),* , 这里*也可以替换为 +,表示至少一次,或者?表示0或1次,这和正则表达式一样

image.png
这里 $x 不仅仅表示一个表达式,而代表的是一个list。
再来看下 <[_]>::into_vec , 这个告诉rust自己去推导元素类型,接着 Box::new([ $( $x ),* ] 和上面匹配一样,将匹配到的元素以逗号为分隔符塞入模板中。我们也可以这样写

  1. ($( $x:expr ),*) => {
  2. {
  3. let mut v = Vec::new();
  4. $( v.push($x); )*
  5. v
  6. }
  7. }

再看第三条规则,rust不会自动处理 trailing commas, 所以标准的做法是添加一条额外的规则来匹配最后一个逗号, 当匹配到额外的逗号后,递归调用自己,并将最后一个逗号抹去。

  1. ( $( $x:expr ),+ ,) => { // if trailing comma is present,
  2. vec![ $( $x ),* ] // retry without it
  3. };

这里我们可以看到在rust中,宏是可以递归调用的