macro_rules
macro_rules是rust中定义宏的主要手段,例如 assert_eq!
macro_rules! assert_eq {
($left:expr, $right:expr) => ({
match (&$left, &$right) {
(left_val, right_val) => {
if !(*left_val == *right_val) {
panic!("assertion failed");
}
}
}
});
}
注意,这里assert_eq后是没有 !
的, !
只有在调用宏的时候才需要。
macro的函数体就是一系列的规则,类似match 例如
(pattern1) => (template1);
(pattern2) => (template2);
在调用宏的时候可以使用 {} [] ()
, 这些都是等价的,我们通常在assert_eq后用(), vec!后用[], macro_rules!后用{}, 这些都是些约定俗成的东西
assert_eq!(gcd(6,10), 2);
assert_eq![gcd(6,10), 2];
assert_eq!{gcd(6,10), 2};
宏展开
当assert_eq! 调用时,rust首先模式匹配参数例如
rust的模式匹配类似正则匹配,与正则不同的是,rust的模式匹配作用于 tokens,这个例子中, $left:expr
告诉rust匹配一个表达式,并赋值$left, 然后匹配 ,
, 最后匹配第二个表达式,并赋值$right
先来看下这个宏,为什么这个宏要创建 left_val和right_val这两个变量?为什么不能直接写成 if $left != $right
? 加入当我们调用 assert_eq!(letters.pop, Some('z'))
的时候,所有的$left, $right都会被直接替换成这两个表达式,这里letters.pop会被多次调用,每次调用都会产生副作用,导致结果不正确,所以我们需要$left $right仅执行一次。
Repetition
再看看vec! 这个宏
let buffer = vec![0_u8; 1000];
let numbers = vec!["udon", "ramen", "soba"];
这个宏有3条rule
macro_rules! vec{
($elem:expr ; &n:expr) => {
::std::vec::from_elem($elem, $n)
};
( $( $x:expr ),* ) => {
<[_]>::into_vec(Box::new([ $( $x ),* ]))
};
( $( $x:expr ),+ ,) => {
vec![ $( $x ),* ]
};
}
vec!["udon", "ramen", "soba"];
匹配的是第二条, $( $x:expr ),*
这个表示匹配表达式 $x:expr, 0或多次,以逗号为分割符,更通用些表示为 $( PATTERN ),*
, 这里*也可以替换为 +,表示至少一次,或者?表示0或1次,这和正则表达式一样
这里 $x 不仅仅表示一个表达式,而代表的是一个list。
再来看下 <[_]>::into_vec
, 这个告诉rust自己去推导元素类型,接着 Box::new([ $( $x ),* ]
和上面匹配一样,将匹配到的元素以逗号为分隔符塞入模板中。我们也可以这样写
($( $x:expr ),*) => {
{
let mut v = Vec::new();
$( v.push($x); )*
v
}
}
再看第三条规则,rust不会自动处理 trailing commas, 所以标准的做法是添加一条额外的规则来匹配最后一个逗号, 当匹配到额外的逗号后,递归调用自己,并将最后一个逗号抹去。
( $( $x:expr ),+ ,) => { // if trailing comma is present,
vec![ $( $x ),* ] // retry without it
};
这里我们可以看到在rust中,宏是可以递归调用的