Document not found (404)
+This URL is invalid, sorry. Please use the navigation bar or search to continue.
+ +diff --git a/.nojekyll b/.nojekyll new file mode 100644 index 0000000..f173110 --- /dev/null +++ b/.nojekyll @@ -0,0 +1 @@ +This file makes sure that Github Pages doesn't process mdBook's output. diff --git a/404.html b/404.html new file mode 100644 index 0000000..e1eeaf0 --- /dev/null +++ b/404.html @@ -0,0 +1,285 @@ + + +
+ + +This URL is invalid, sorry. Please use the navigation bar or search to continue.
+ +本章将介绍 Rust 的声明宏系统:macro_rules!
。
在这一章中有两种不同的介绍,一个 讲思路,另一个 +讲实践。
+前者会向你阐述一个完整而详尽的系统如何工作,而后者将涵盖更多的实际例子。
+因此,思路介绍 +是为那些只希望声明宏系统作为一个整体得到解释的人而设计的,而 +实践介绍 则指导人们通过实现单个宏。
+在这两个介绍之后,本章还提供了一些常规且有用的 模式 +和 构件,用于创建功能丰富的声明宏。
+关于声明宏的其他资源:
+++ +注意:本书在讨论声明宏时,通常会使用术语 mbe (Macro-By-Example)、 mbe macro 或 +
+macro_rules!
。
可重用的宏代码片段 (reusable snippets) 。(也可称作“轮子”)
+ +++临时信息:需要更合适的例子。 +该用例采用 Rust 分组机制无法表示的匹配嵌套结构, +实在是过于特殊,因此不适作为例子使用。
+
+ ++
+macro_rules! abacus { + ((- $($moves:tt)*) -> (+ $($count:tt)*)) => { + abacus!(($($moves)*) -> ($($count)*)) + }; + ((- $($moves:tt)*) -> ($($count:tt)*)) => { + abacus!(($($moves)*) -> (- $($count)*)) + }; + ((+ $($moves:tt)*) -> (- $($count:tt)*)) => { + abacus!(($($moves)*) -> ($($count)*)) + }; + ((+ $($moves:tt)*) -> ($($count:tt)*)) => { + abacus!(($($moves)*) -> (+ $($count)*)) + }; + + // Check if the final result is zero. + (() -> ()) => { true }; + (() -> ($($count:tt)+)) => { false }; +} + +fn main() { + let equals_zero = abacus!((++-+-+++--++---++----+) -> ()); + assert_eq!(equals_zero, true); +}
这个例子所用的技巧用在如下情况: +记录的计数会发生变化,且初始值为零或在零附近,且必须支持如下操作:
+数值 n 将由一组共 n 个相同的特定标记来表示。
+对数值的修改操作将采用 下推累积 模式由递归调用完成。
+假设所采用的特定标记是 x
,则上述操作可实现为:
($($count:tt)*)
并替换为(x $($count)*)
。(x $($count:tt)*)
并替换为($($count)*)
。()
。(x)
。(x x)
。作用于计数值的操作将所选的标记来回摆动,如同算盘摆动算子。1
+在这句极度单薄的辩解下,隐藏着选用此名称的 真实 理由: +避免造出又一个名含“标记”的术语。今天就该跟你认识的作者谈谈避免 +语义饱和 吧! +公平来讲,本来也可以称它为 +“一元计数(unary counting)” 。
+在想表示负数的情况下,值 -n 可被表示成 n 个相同的其它标记。
+在上例中,值 +n 被表示成 n 个 +
标记,而值 -m 被表示成 m 个 -
标记。
有负数的情况下操作起来稍微复杂一些,
+增减操作在当前数值为负时实际上互换了角色。
+给定 +
和 -
分别作为正数与负数标记,相应操作的实现将变成:
()
并替换为 (+)
(- $($count:tt)*)
并替换为 ($($count)*)
($($count:tt)+)
并替换为 (+ $($count)+)
()
并替换为 (-)
(+ $($count:tt)*)
并替换为 ($($count)*)
($($count:tt)+)
并替换为 (- $($count)+)
()
(+)
(-)
(++)
(--)
注意在顶部的示例中,某些规则被合并到一起了
+(举例来说,对 ()
及 ($($count:tt)+)
的增加操作被合并为对
+($($count:tt)*)
的增加操作)。
如果想要提取出所计数目的实际值,可再使用普通的 +计数宏 。对上例来说,终结规则可换为:
+macro_rules! abacus {
+ // ...
+
+ // 下列规则将计数替换成实际值的表达式
+ (() -> ()) => {0};
+ (() -> (- $($count:tt)*)) => {
+ - ( count_tts!($( $count_tts:tt )*) )
+ };
+ (() -> (+ $($count:tt)*)) => {
+ count_tts!($( $count_tts:tt )*)
+ };
+}
+
+// 计数一章任选一个宏
+macro_rules! count_tts {
+ // ...
+}
+++仅限此例: +严格来说,想要达到此例的效果,没必要做的这么复杂。 +如果你不需要在宏中匹配所计的值,可直接采用重复来更加高效地实现:
++macro_rules! abacus { + (-) => {-1}; + (+) => {1}; + ($( $moves:tt )*) => { + 0 $(+ abacus!($moves))* + } +} +
++译者注:这章原作者的表述实在过于啰嗦,但是这个例子的确很有意思。 +基于这个例子框架,我给出如下浅显而完整的样例代码(可编辑运行):
+
+macro_rules! abacus { + ((- $($moves:tt)*) -> (+ $($count:tt)*)) => { + { + println!("{} [-]{} | [+]{}", "-+1", stringify!($($moves)*), stringify!($($count)*)); + abacus!(($($moves)*) -> ($($count)*)) + } + }; + ((- $($moves:tt)*) -> ($($count:tt)*)) => { + { + println!("{} [-]{} | - {}", "- 2", stringify!($($moves)*), stringify!($($count)*)); + abacus!(($($moves)*) -> (- $($count)*)) + } + }; + ((+ $($moves:tt)*) -> (- $($count:tt)*)) => { + { + println!("{} [+]{} | [-]{}", "+-3", stringify!($($moves)*), stringify!($($count)*)); + abacus!(($($moves)*) -> ($($count)*)) + } + }; + ((+ $($moves:tt)*) -> ($($count:tt)*)) => { + { + println!("{} [+]{} | + {}", "+ 4", stringify!($($moves)*), stringify!($($count)*)); + abacus!(($($moves)*) -> (+ $($count)*)) + } + }; + + (() -> ()) => {0}; + (() -> (- $($count:tt)*)) => {{-1 + abacus!(() -> ($($count)*)) }}; + (() -> (+ $($count:tt)*)) => {{1 + abacus!(() -> ($($count)*)) }}; +} + +fn main() { + println!("算盘游戏:左边与右边异号时抵消;非异号时,把左边的符号转移到右边;左边无符号时,游戏结束,计算右边得分"); + println!("图示注解:左右符号消耗情况,分支编号,[消失的符号] 左边情况 | [消失的符号] 右边情况\n"); + + println!("计数结果:{}\n", abacus!((++-+-+) -> (--+-+-))); + println!("计数结果:{}\n", abacus!((++-+-+) -> (++-+-+))); + println!("计数结果:{}\n", abacus!((---+) -> ())); + println!("计数结果:{}\n", abacus!((++-+-+) -> ())); + println!("计数结果:{}\n", abacus!((++-+-+++--++---++----+) -> ())); // 这是作者给的例子 :) +}
打印结果:
+算盘游戏:左边与右边异号时抵消;非异号时,把左边的符号转移到右边;左边无符号时,游戏结束,计算右边得分
+图示注解:左右符号消耗情况,分支编号,[消失的符号] 左边情况 | [消失的符号] 右边情况
+
++-3 [+]+ - + - + | [-]- + - + -
++-3 [+]- + - + | [-]+ - + -
+-+1 [-]+ - + | [+]- + -
++-3 [+]- + | [-]+ -
+-+1 [-]+ | [+]-
++-3 [+] | [-]
+计数结果:0
+
++ 4 [+]+ - + - + | + + + - + - +
++ 4 [+]- + - + | + + + + - + - +
+-+1 [-]+ - + | [+]+ + + - + - +
++ 4 [+]- + | + + + + - + - +
+-+1 [-]+ | [+]+ + + - + - +
++ 4 [+] | + + + + - + - +
+计数结果:4
+
+- 2 [-]- - + | -
+- 2 [-]- + | - -
+- 2 [-]+ | - - -
++-3 [+] | [-]- -
+计数结果:-2
+
++ 4 [+]+ - + - + | +
++ 4 [+]- + - + | + +
+-+1 [-]+ - + | [+]+
++ 4 [+]- + | + +
+-+1 [-]+ | [+]+
++ 4 [+] | + +
+计数结果:2
+
++ 4 [+]+ - + - + + + - - + + - - - + + - - - - + | +
++ 4 [+]- + - + + + - - + + - - - + + - - - - + | + +
+-+1 [-]+ - + + + - - + + - - - + + - - - - + | [+]+
++ 4 [+]- + + + - - + + - - - + + - - - - + | + +
+-+1 [-]+ + + - - + + - - - + + - - - - + | [+]+
++ 4 [+]+ + - - + + - - - + + - - - - + | + +
++ 4 [+]+ - - + + - - - + + - - - - + | + + +
++ 4 [+]- - + + - - - + + - - - - + | + + + +
+-+1 [-]- + + - - - + + - - - - + | [+]+ + +
+-+1 [-]+ + - - - + + - - - - + | [+]+ +
++ 4 [+]+ - - - + + - - - - + | + + +
++ 4 [+]- - - + + - - - - + | + + + +
+-+1 [-]- - + + - - - - + | [+]+ + +
+-+1 [-]- + + - - - - + | [+]+ +
+-+1 [-]+ + - - - - + | [+]+
++ 4 [+]+ - - - - + | + +
++ 4 [+]- - - - + | + + +
+-+1 [-]- - - + | [+]+ +
+-+1 [-]- - + | [+]+
+-+1 [-]- + | [+]
+- 2 [-]+ | -
++-3 [+] | [-]
+计数结果:0
+
+
+ 在替换 tt
时,Rust 的解析器并不十分可靠。
+当它期望得到某类特定的语法结构时,
+如果摆在它面前的是一坨替换后的 tt
标记,就有可能出现问题。
+解析器常常直接选择放弃解析,而非尝试去解析它们。
+在这类情况中,就要用到 AST 强制转换(简称“强转”)。
+#![allow(dead_code)] + +macro_rules! as_expr { ($e:expr) => {$e} } +macro_rules! as_item { ($i:item) => {$i} } +macro_rules! as_pat { ($p:pat) => {$p} } +macro_rules! as_stmt { ($s:stmt) => {$s} } +macro_rules! as_ty { ($t:ty) => {$t} } + +fn main() { + as_item!{struct Dummy;} + + as_stmt!(let as_pat!(_): as_ty!(_) = as_expr!(42)); +}
这些强制变换经常与 下推累积 宏一同使用,
+以使解析器能够将最终输出的 tt
序列当作某类特定的语法结构来对待。
注意:之所以只有这几种强转宏, +是由宏 可以展开成什么 所决定的, +而不是由宏能够捕捉哪些东西所决定的。
+ +在宏中计数是一项让人吃惊的难搞的活儿。 +最简单的方式是采用反复替换 (repetition with replacement) 。
++macro_rules! replace_expr { + ($_t:tt $sub:expr) => {$sub}; +} + +macro_rules! count_tts { + ($($tts:tt)*) => {0usize $(+ replace_expr!($tts 1usize))*}; +} + +fn main() { + assert_eq!(count_tts!(0 1 2), 3); +}
对于小数目来说,这方法不错,但当输入量到达 500 1左右的标记时,
+很可能让编译器崩溃。想想吧,输出的结果将类似:
0usize + 1usize + /* ~500 `+ 1usize`s */ + 1usize
+编译器必须把这一大串解析成一棵 AST , +那可会是一棵完美失衡的 500 多级深的二叉树。
+译者注:500 这个数据过时了,例子见下面 递归 第三个代码块。
+递归 (recursion) 是个老套路。
++macro_rules! count_tts { + () => {0usize}; + ($_head:tt $($tail:tt)*) => {1usize + count_tts!($($tail)*)}; +} + +fn main() { + assert_eq!(count_tts!(0 1 2), 3); +}
++注意:对于
+rustc
1.2 来说,很不幸, +编译器在处理大数量的类型未知的整型字面值时将会出现性能问题。 +我们此处显式采用usize
类型就是为了避免这种不幸。如果这样做并不合适(比如说,当类型必须可替换时), +可通过
+as
来减轻问题。(比如,0 as $ty
、1 as $ty
等)。
这方法管用,但很快就会超出宏递归的次数限制( +目前 +是 128 )。
+与重复替换不同的是,可通过增加匹配分支来增加可处理的输入面值。
+以下为增加匹配分支的改进代码2,如果把前三个分支注释掉,看看编译器会提示啥 :)
++macro_rules! count_tts { + ($_a:tt $_b:tt $_c:tt $_d:tt $_e:tt + $_f:tt $_g:tt $_h:tt $_i:tt $_j:tt + $_k:tt $_l:tt $_m:tt $_n:tt $_o:tt + $_p:tt $_q:tt $_r:tt $_s:tt $_t:tt + $($tail:tt)*) + => {20usize + count_tts!($($tail)*)}; + ($_a:tt $_b:tt $_c:tt $_d:tt $_e:tt + $_f:tt $_g:tt $_h:tt $_i:tt $_j:tt + $($tail:tt)*) + => {10usize + count_tts!($($tail)*)}; + ($_a:tt $_b:tt $_c:tt $_d:tt $_e:tt + $($tail:tt)*) + => {5usize + count_tts!($($tail)*)}; + ($_a:tt + $($tail:tt)*) + => {1usize + count_tts!($($tail)*)}; + () => {0usize}; +} + +fn main() { + assert_eq!(700, count_tts!( + ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, + ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, + + ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, + ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, + + ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, + ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, + + ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, + ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, + + ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, + ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, + + ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, + ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, + + ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, + ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, + )); +}
可以复制下面的例子运行看看,里面包含递归和反复匹配(代码已隐藏)两种方法。
++macro_rules! count_tts { + ($_a:tt $_b:tt $_c:tt $_d:tt $_e:tt + $_f:tt $_g:tt $_h:tt $_i:tt $_j:tt + $_k:tt $_l:tt $_m:tt $_n:tt $_o:tt + $_p:tt $_q:tt $_r:tt $_s:tt $_t:tt + $($tail:tt)*) + => {20usize + count_tts!($($tail)*)}; + ($_a:tt $_b:tt $_c:tt $_d:tt $_e:tt + $_f:tt $_g:tt $_h:tt $_i:tt $_j:tt + $($tail:tt)*) + => {10usize + count_tts!($($tail)*)}; + ($_a:tt $_b:tt $_c:tt $_d:tt $_e:tt + $($tail:tt)*) + => {5usize + count_tts!($($tail)*)}; + ($_a:tt + $($tail:tt)*) + => {1usize + count_tts!($($tail)*)}; + () => {0usize}; +} + +// 可试试“反复替代”的方式计数 +// --snippet-- +// macro_rules! replace_expr { +// ($_t:tt $sub:expr) => { +// $sub +// }; +// } +// +// macro_rules! count_tts { +// ($($tts:tt)*) => {0usize $(+ replace_expr!($tts 1usize))*}; +// } + +fn main() { + assert_eq!(2500, + count_tts!( + ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, + ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, + + ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, + ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, + + ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, + ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, + + ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, + ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, + + // --snippet-- + ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, + ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, + + ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, + ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, + + ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, + ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, + + ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, + ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, + + ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, + ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, + + ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, + ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, + + ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, + ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, + + ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, + ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, + + ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, + ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, + + ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, + ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, + + ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, + ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, + + ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, + ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, + + ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, + ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, + + ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, + ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, + + ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, + ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, + + ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, + ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, + + ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, + ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, + + ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, + ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, + + ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, + ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, + + ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, + ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, + + ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, + ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, + + // 默认的递归限制让改进的递归代码也无法继续下去了 + // 反复替换的代码还能够运行,但明显效率不会很高 + // ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, + // ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, + )); +}
译者注:如果不显式提高 128 的递归限制的话, +这个例子中,增加匹配分支办法可以处理最多 \(20 \times 128 = 2560 \) 个标记。
+第三种方法,是帮助编译器构建一个深度较小的 AST ,以避免栈溢出。
+可以通过构造数组,并调用其 len
方法来做到。(slice length)
+macro_rules! replace_expr { + ($_t:tt $sub:expr) => {$sub}; +} + +macro_rules! count_tts { + ($($tts:tt)*) => {<[()]>::len(&[$(replace_expr!($tts ())),*])}; +} + +fn main() { + assert_eq!(count_tts!(0 1 2), 3); + + const N: usize = count_tts!(0 1 2); + let array = [0; N]; + println!("{:?}", array); +}
经过测试,这种方法可处理高达 10000 个标记数,可能还能多上不少。3
+而且可以用于常量表达式,比如当作在 const
值或定长数组的长度值。4
所以基本上此方法是 首选 。
+译者注:这个具体的数据可能也过时了,但这个方法的确是高效的。
+译者注:原作时这个方法无法用于常量,现在无此限制。
+当你需要统计 互不相同的标识符 的数量时, +可以利用枚举体的 +numeric cast +功能来达到统计成员(即标识符)个数。
++macro_rules! count_idents { + ($($idents:ident),* $(,)*) => { + { + #[allow(dead_code, non_camel_case_types)] + enum Idents { $($idents,)* __CountIdentsLast } + const COUNT: u32 = Idents::__CountIdentsLast as u32; + COUNT + } + }; +} + +fn main() { + const COUNT: u32 = count_idents!(A, B, C); + assert_eq!(COUNT, 3); +}
此方法有两大缺陷:
+__CountIdentsLast
5 位置上的标识符)的字面值也是输入之一,
+那么宏调用就会失败,因为 enum
中包含重复变量。译者注:__CountIdentsLast
只是一个自定义的标识符,重点在于它处于枚举成员的最后一位。
另一个递归方法,但是使用了 位操作 (bit operations) 6:
++macro_rules! count_tts { + () => { 0 }; + ($odd:tt $($a:tt $b:tt)*) => { (count_tts!($($a)*) << 1) | 1 }; + ($($a:tt $even:tt)*) => { count_tts!($($a)*) << 1 }; +} + +fn main() { + assert_eq!(count_tts!(0 1 2), 3); +}
这种方法非常聪明。
+只要它是偶数个,就能有效地将其输入减半,
+然后将计数器乘以 2(或者在这种情况下,向左移1位)。
+因为由于前一次左移位,此时最低位必须为 0 ,重复直到我们达到基本规则 () => 0
。
+如果输入是奇数个,则从第二个输入开始减半,最终将结果进行 或运算(这等效于加 1)。
这样做的好处是,生成计数器的 AST 表达式将以 O(log(n))
而不是 O(n)
复杂度增长。
+请注意,这仍然可能达到递归限制。
让我们手动分析中间的过程:
+count_tts!(0 0 0 0 0 0 0 0 0 0);
+由于我们的标记树数量为偶数(10),因此该调用将与第三条规则匹配。
+该匹配分支把奇数项的标记树命名给 $a
,偶数项的标记树命名成 $b
,
+但是只会对奇数项 $a
展开,这意味着有效地抛弃所有偶数项,切断了一半的输入。
+因此,调用现在变为:
count_tts!(0 0 0 0 0) << 1;
+现在,该调用将匹配第二条规则,因为其输入的令牌树数量为奇数。 +在这种情况下,第一个标记树将被丢弃以再次让输入变成偶数个, +然后可以在调用中再次进行减半步骤。 +此时,我们可以将奇数时丢弃的一项计数为1,然后再乘以2,因为我们也减半了。
+((count_tts!(0 0) << 1) | 1) << 1;
+((count_tts!(0) << 1 << 1) | 1) << 1;
+(((count_tts!() | 1) << 1 << 1) | 1) << 1;
+((((0 << 1) | 1) << 1 << 1) | 1) << 1;
+现在,要检查是否正确分析了扩展过程,
+我们可以使用 debugging
调试工具。
+展开宏后,我们应该得到:
((((0 << 1) | 1) << 1 << 1) | 1) << 1;
+没有任何差错,太棒了!
+++译者注:以下内容为译者自行补充这小节提到的调试。 +注意:我这里使用的加、乘运算与上面提到的位运算是一样的。
+
+#![allow(unused)] +macro_rules! count_tts { + () => { 0 }; + ($odd:tt $($a:tt $b:tt)*) => { (count_tts!($($a)*) *2) + 1 }; + ($($a:tt $even:tt)*) => { count_tts!($($a)*) *2 }; +} + +fn main() { + count_tts!(0 1 2 3 4 5 6 7 8 9 10); +}
调试方法(必须在 nightly 版本下):
+cargo rustc -- -Z trace-macros
+得到:note: trace_macro
+ --> src/main.rs:9:5
+ |
+9 | count_tts!(0 1 2 3 4 5 6 7 8 9 10);
+ | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+ |
+ = note: expanding `count_tts! { 0 1 2 3 4 5 6 7 8 9 10 }`
+ = note: to `(count_tts! (1 3 5 7 9) * 2) + 1`
+ = note: expanding `count_tts! { 1 3 5 7 9 }`
+ = note: to `(count_tts! (3 7) * 2) + 1`
+ = note: expanding `count_tts! { 3 7 }`
+ = note: to `count_tts! (3) * 2`
+ = note: expanding `count_tts! { 3 }`
+ = note: to `(count_tts! () * 2) + 1`
+ = note: expanding `count_tts! { }`
+ = note: to `0`
+
+cargo expand
,得到:#![feature(prelude_import)]
+#![allow(unused)]
+#[prelude_import]
+use std::prelude::rust_2018::*;
+#[macro_use]
+extern crate std;
+fn main() {
+ (((((0 * 2) + 1) * 2 * 2) + 1) * 2) + 1;
+}
+
+这种方法的归功于 Reddit 用户
+YatoRust
。
在有些情况下解析某些 Rust items 会很有用。 +这一章会展示一些能够解析 Rust 中更复杂的 items 的宏。
+这些宏目的不是解析整个 items 语法,而是解析通用、有用的部分, +解析的方式也不会太复杂。 +也就是说,我们不会涉及解析 泛型 之类的东西。
+重点在于宏的匹配方式 (matchers) ;展开的部分 ( Reference 里使用的术语叫做 transcribers ), +仅仅用作例子,不需要特别关心它。
++macro_rules! function_item_matcher { + ( + + $( #[$meta:meta] )* + // ^~~~attributes~~~~^ + $vis:vis fn $name:ident ( $( $arg_name:ident : $arg_ty:ty ),* $(,)? ) + // ^~~~~~~~~~~~~~~~argument list!~~~~~~~~~~~~~~^ + $( -> $ret_ty:ty )? + // ^~~~return type~~~^ + { $($tt:tt)* } + // ^~~~~body~~~~^ + ) => { + $( #[$meta] )* + $vis fn $name ( $( $arg_name : $arg_ty ),* ) $( -> $ret_ty )? { $($tt)* } + } +} + +function_item_matcher!( + #[inline] + #[cold] + pub fn foo(bar: i32, baz: i32, ) -> String { + format!("{} {}", bar, baz) + } +); + +fn main() { + assert_eq!(foo(13, 37), "13 37"); +}
这是一个简单的匹配函数的例子,
+传入宏的函数不能包含 unsafe
、async
、泛型和 where 语句。
+如果需要解析这些内容,则最好使用 proc-macro
(过程宏) 代替。
这个例子可以检查函数签名,从中生成一些额外的东西,
+然后再重新返回 (re-emit) 整个函数。
+有点像 Derive
过程宏,虽然功能没那么强大,但是是为函数服务的
+( Derive
不作用于函数)。
++理想情况下,我们对参数捕获宁愿使用
+pat
分类符,而不是ident
分类符, +但这里目前不被允许(因为前者的跟随限制,不允许其后使用:
)。 +幸好在函数签名里面不常使用模式 (pat
) ,所以这个例子还不错。
有时我们想解析方法 (methods),方法就是通过 self
的某种形式指向对象的函数。
+这让事情变得棘手多了。
++WIP (待完善)
+
+macro_rules! struct_item_matcher { + // Unit-Struct + ( + $( #[$meta:meta] )* + // ^~~~attributes~~~~^ + $vis:vis struct $name:ident; + ) => { + $( #[$meta] )* + $vis struct $name; + }; + + // Tuple-Struct + ( + $( #[$meta:meta] )* + // ^~~~attributes~~~~^ + $vis:vis struct $name:ident ( + $( + $( #[$field_meta:meta] )* + // ^~~~field attributes~~~~^ + $field_vis:vis $field_ty:ty + // ^~~~~~a single field~~~~~~^ + ),* + $(,)? ); + ) => { + $( #[$meta] )* + $vis struct $name ( + $( + $( #[$field_meta] )* + $field_vis $field_ty + ),* + ); + }; + + // Named-Struct + ( + $( #[$meta:meta] )* + // ^~~~attributes~~~~^ + $vis:vis struct $name:ident { + $( + $( #[$field_meta:meta] )* + // ^~~~field attributes~~~!^ + $field_vis:vis $field_name:ident : $field_ty:ty + // ^~~~~~~~~~~~~~~~~a single field~~~~~~~~~~~~~~~^ + ),* + $(,)? } + ) => { + $( #[$meta] )* + $vis struct $name { + $( + $( #[$field_meta] )* + $field_vis $field_name : $field_ty + ),* + } + } +} + +struct_item_matcher!( + #[allow(dead_code)] + #[derive(Copy, Clone)] + pub(crate) struct Foo { + pub bar: i32, + baz: &'static str, + qux: f32 + } +); +struct_item_matcher!( + #[derive(Copy, Clone)] + pub(crate) struct Bar; +); +struct_item_matcher!( + #[derive(Clone)] + pub(crate) struct Baz (i32, pub f32, String); +); +fn main() { + let _: Foo = Foo { bar: 42, baz: "macros can be nice", qux: 3.14, }; + let _: Bar = Bar; + let _: Baz = Baz(2, 0.1234, String::new()); +}
解析枚举体比解析结构体更复杂一点,所以会用上 模式 这章讨论的技巧:
+TT
撕咬机 和 内用规则 。
不是重新构造被解析的枚举体,而是只访问枚举体所有的标记 (tokens), +因为重构枚举体将需要我们再通过 下推累积 +临时组合所有已解析的标记 (tokens) 。
++ +macro_rules! enum_item_matcher { + // tuple variant + (@variant $variant:ident ( + $( + $( #[$field_meta:meta] )* + // ^~~~field attributes~~~~^ + $field_vis:vis $field_ty:ty + // ^~~~~~a single field~~~~~~^ + ),* $(,)? + //∨~~rest of input~~∨ + ) $(, $($tt:tt)* )? ) => { + + // process rest of the enum + $( enum_item_matcher!(@variant $( $tt )*); )? + }; + + // named variant + (@variant $variant:ident { + $( + $( #[$field_meta:meta] )* + // ^~~~field attributes~~~!^ + $field_vis:vis $field_name:ident : $field_ty:ty + // ^~~~~~~~~~~~~~~~~a single field~~~~~~~~~~~~~~~^ + ),* $(,)? + //∨~~rest of input~~∨ + } $(, $($tt:tt)* )? ) => { + // process rest of the enum + $( enum_item_matcher!(@variant $( $tt )*); )? + }; + + // unit variant + (@variant $variant:ident $(, $($tt:tt)* )? ) => { + // process rest of the enum + $( enum_item_matcher!(@variant $( $tt )*); )? + }; + + // trailing comma + (@variant ,) => {}; + // base case + (@variant) => {}; + + // entry point + ( + $( #[$meta:meta] )* + $vis:vis enum $name:ident { + $($tt:tt)* + } + ) => { + enum_item_matcher!(@variant $($tt)*); + }; +} + +enum_item_matcher!( + #[derive(Copy, Clone)] + pub(crate) enum Foo { + Bar, + Baz, + } +); +enum_item_matcher!( + #[derive(Copy, Clone)] + pub(crate) enum Bar { + Foo(i32, f32), + Bar, + Baz(), + } +); +enum_item_matcher!( + #[derive(Clone)] + pub(crate) enum Baz {} +); + +fn main() {}
这一节会介绍 Rust 的声明宏系统,解释该系统如何作为整体运作。
+首先会深入构造语法及其关键部分,然后介绍你至少应该了解的通用信息。
+macro_rules!
有了前述知识,我们终于可以介绍 macro_rules!
了。如前所述,macro_rules!
+本身就是一个语法扩展,也就是从技术上说,它并不是 Rust 语法的一部分。它的形式如下:
macro_rules! $name {
+ $rule0 ;
+ $rule1 ;
+ // …
+ $ruleN ;
+}
+至少得有一条规则,而且最后一条规则后面的分号可被省略。规则里你可以使用大/中/小括号:
+{}
、[]
、()
1。每条“规则”都形如:
($matcher) => {$expansion}
+
+译者注:它们的英文名称有时候很重要,因为如果你不认识英文名称的话,会比较难读懂文档(比如
+syn
)。braces {}
、brackets []
、parentheses ()
。
分组符号可以是任意一种括号,但处于习惯,在模式匹配 (matcher) 外侧使用小括号、展开 +(expansion 也可以叫做 transcriber) 外侧使用大括号。
+注意:在规则里选择哪种括号并不会影响宏调用。
+而且,实际上,你也可以在调用宏时使用这三种中任意一种括号,只不过使用 { ... }
或者 ( ... );
+的话会有所不同(关注点在于末尾跟随的分号 ;
)。有末尾分号的宏调用总是会被解析成一个条目 (item)。
如果你好奇的话,macro_rules!
的调用将被展开成什么?答案是:空 (nothing)。至少,在 AST
+中它被展开为空。它所影响的是编译器内部的结构,以将该宏注册 (register)
+进去。因此,技术上讲你可以在任何一个空展开合法的位置使用 macro_rules!
。
++译者注:这里提到两种情况,定义声明宏和使用(或者说调用)声明宏。而且,在括号的选取上:
++
+- 定义的规则不关心
+($matcher) => {$expansion}
中的外层括号类型,但 matcher 和 expansion +之内的括号属于匹配和展开的内容,所以它们内部使用什么括号取决于你需要什么语法。- 假如使用
+m!
这个宏,如果该宏展开成条目,则必须使用m! { ... }
或者m!( ... );
; +如果该宏展开成表达式,你可以使用m! { ... }
或者m!( ... )
或者m![ ... ]
。- 实际上,定义宏的括号遵循习惯就好,而使用宏的括号用错的话,只需仔细阅读编译器给你的错误信息,和以上第 +2 点,就知道怎么改了。
+
当一个宏被调用时,macro_rules!
解释器将按照声明顺序一一检查规则。
对每条规则,它都将尝试将输入标记树的内容与该规则的 matcher
进行匹配。某个 matcher 2
+必须与输入完全匹配才被认为是一次匹配。
译者注:为了简单起见,我不翻译 matcher 这个术语,它指的是被匹配的部分,也就是声明宏规则的前半段。
+如果输入与某个 matcher 相匹配,则该调用将替换成相应的展开内容 (expansion
) ;否则,将尝试匹配下条规则。
如果所有规则均匹配失败,则宏展开失败并报错。
+最简单的例子是空 matcher:
+macro_rules! four {
+ () => { 1 + 3 };
+}
+当且仅当匹配到空的输入时,匹配成功,即 four!()
、four![]
或 four!{}
三种方式调用是匹配成功的 。
注意所用的分组标记并不需要匹配定义时采用的分组标记,因为实际上分组标记并未传给调用。
+也就是说,你可以通过 four![]
调用上述宏,此调用仍将被视作匹配成功。只有输入的内容才会被纳入匹配考量范围。
matcher 中也可以包含字面上3的标记树,这些标记树必须被完全匹配。将整个对应标记树在相应位置写下即可。
+比如,要匹配标记序列 4 fn ['spang "whammo"] @_@
,我们可以这样写:
macro_rules! gibberish {
+ (4 fn ['spang "whammo"] @_@) => {...};
+}
+使用 gibberish!(4 fn ['spang "whammo"] @_@])
即可成功匹配和调用。
你能写出什么标记树,就可以使用什么标记树。
+译者注:这里不是指 Rust 的“字面值”,而是指不考虑含义的标记,比如这个例子中 fn
和 []
都不是
+Rust 的 literal 标记 (token),而是 keyword 和 delimiter
+标记,或者从下面谈到的元变量角度看,它们可以被 ident
或者 tt
分类符捕获。
matcher 还可以包含捕获 (captures)。即基于某种通用语法类别来匹配输入,并将结果捕获到元变量 (metavariable) +中,然后将替换元变量到输出。
+捕获的书写方式是:先写美元符号 $
,然后跟一个标识符,然后是冒号 :
,最后是捕获方式,比如 $e:expr
。
捕获方式又被称作“片段分类符” (fragment-specifier),必须是以下一种:
+block
:一个块(比如一块语句或者由大括号包围的一个表达式)expr
:一个表达式 (expression)ident
:一个标识符 (identifier),包括关键字 (keywords)item
:一个条目(比如函数、结构体、模块、impl
块)lifetime
:一个生命周期注解(比如 'foo
、'static
)literal
:一个字面值(比如 "Hello World!"
、3.14
、'🦀'
)meta
:一个元信息(比如 #[...]
和 #![...]
属性内部的东西)pat
:一个模式 (pattern)path
:一条路径(比如 foo
、::std::mem::replace
、transmute::<_, int>
)stmt
:一条语句 (statement)tt
:单棵标记树ty
:一个类型vis
:一个可能为空的可视标识符(比如 pub
、pub(in crate)
)关于片段分类符更深入的描述请阅读本书的片段分类符一章。
+比如以下声明宏捕获一个表达式输入到元变量 $e
:
macro_rules! one_expression {
+ ($e:expr) => {...};
+}
+元变量对 Rust 编译器的解析器产生影响,而解析器也会确保元变量总是被“正确无误”地解析。
+expr
元变量总是捕获完整且符合 Rust 编译版本的表达式。
你可以在有限的情况下同时结合字面上的标记树和元变量。(见 Metavariables and Expansion Redux 一节)
+当元变量已经在 matcher 中确定之后,你只需要写 $name
就能引用元变量。比如:
macro_rules! times_five {
+ ($e:expr) => { 5 * $e };
+}
+元变量被替换成完整的 AST 节点,这很像宏展开。这也意味着被 $e
+捕获的任何标记序列都会被解析成单个完整的表达式。
你也可以一个 matcher 中捕获多个元变量:
+macro_rules! multiply_add {
+ ($a:expr, $b:expr, $c:expr) => { $a * ($b + $c) };
+}
+然后在 expansion 中使用任意次数的元变量:
+macro_rules! discard {
+ ($e:expr) => {};
+}
+macro_rules! repeat {
+ ($e:expr) => { $e; $e; $e; };
+}
+有一个特殊的元变量叫做 $crate
,它用来指代当前 crate 。
matcher 可以有反复捕获 (repetition),这使得匹配一连串标记 (token)
+成为可能。反复捕获的一般形式为 $ ( ... ) sep rep
。
$
是字面上的美元符号标记( ... )
是被反复匹配的模式,由小括号包围。sep
是可选的分隔标记。它不能是括号或者反复操作符 rep
。常用例子有 ,
和 ;
。rep
是必须的重复操作符。当前可以是:
+?
:表示最多一次重复,所以此时不能前跟分隔标记。*
:表示零次或多次重复。+
:表示一次或多次重复。反复捕获中可以包含任意其他的有效 matcher,比如字面上的标记树、元变量以及任意嵌套的反复捕获。
+在 expansion 中,使用被反复捕获的内容时,也采用相同的语法。而且被反复捕获的元变量只能存在于反复语法内。
+举例来说,下面这个宏将每一个元素转换成字符串:它先匹配零或多个由逗号分隔的表达式,并分别将它们构造成
+Vec
的表达式。
+macro_rules! vec_strs { + ( + // 开始反复捕获 + $( + // 每个反复必须包含一个表达式 + $element:expr + ) + // 由逗号分隔 + , + // 0 或多次 + * + ) => { + // 在这个块内用大括号括起来,然后在里面写多条语句 + { + let mut v = Vec::new(); + + // 开始反复捕获 + $( + // 每个反复会展开成下面表达式,其中 $element 被换成相应被捕获的表达式 + v.push(format!("{}", $element)); + )* + + v + } + }; +} + +fn main() { + let s = vec_strs![1, "a", true, 3.14159f32]; + assert_eq!(s, &["1", "a", "true", "3.14159"]); +}
你可以在一个反复语句里面使用多次和多个元变量,只要这些元变量以相同的次数重复。所以下面的宏代码正常运行:
++macro_rules! repeat_two { + ($($i:ident)*, $($i2:ident)*) => { + $( let $i: (); let $i2: (); )* + } +} + +fn main () { + repeat_two!( a b c d e f, u v w x y z ); +}
但是这下面的不能运行:
++macro_rules! repeat_two { + ($($i:ident)*, $($i2:ident)*) => { + $( let $i: (); let $i2: (); )* + } +} + +fn main() { + repeat_two!( a b c d e f, x y z ); +}
运行报以下错误:
+error: meta-variable `i` repeats 6 times, but `i2` repeats 3 times
+ --> src/main.rs:6:10
+ |
+6 | $( let $i: (); let $i2: (); )*
+ | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+++RFC: rfcs#1584
+Tracking Issue: rust#83527
+Feature:
+#![feature(macro_metavar_expr)]
transcriber4 可以包含所谓的元变量表达 (metavariable expressions)。
+元变量表达式为 transcriber 提供了关于元变量的信息 —— 这些信息是不容易获得的。
+目前除了 $$
表达式外,它们的一般形式都是 $ { op(...) }
:即除了 $$
以外的所有元变量表达式都涉及反复。
可以使用以下表达式(其中 ident
是所绑定的元变量的名称,而 depth
是整型字面值):
${count(ident)}
:最里层反复 $ident
的总次数,相当于 ${count(ident, 0)}
${count(ident,depth)}
:第 depth
层反复 $ident
的次数${index()}
:最里层反复的当前反复的索引,相当于 ${index(0)}
${index(depth)}
:在第 depth
层处当前反复的索引,向外计数${length()}
:最里层反复的重复次数,相当于 ${length(0)}
${length(depth)}
:在第 depth
层反复的次数,向外计数${ignore(ident)}
:绑定 $ident
进行重复,并展开成空$$
:展开为单个 $
,这会有效地转义 $
标记,因此它不会被展开(转写)即 expansion,指展开的部分,是每条声明宏规则的后半段。
++
想了解完整的定义语法,可以参考 Rust Reference 书的 Macros By Example 一章。
+ +Position | +Input | +inits |
+ recur |
+
---|---|---|---|
a[n] = $($inits:expr),+ , ... , $recur:expr
+ ⌂ |
+ a[n] = 0, 1, ..., a[n-1] + a[n-2] |
+ + | + |
a[n] = $($inits:expr),+ , ... , $recur:expr
+ ⌂ |
+ [n] = 0, 1, ..., a[n-1] + a[n-2] |
+ + | + |
a[n] = $($inits:expr),+ , ... , $recur:expr
+ ⌂ |
+ n] = 0, 1, ..., a[n-1] + a[n-2] |
+ + | + |
a[n] = $($inits:expr),+ , ... , $recur:expr
+ ⌂ |
+ ] = 0, 1, ..., a[n-1] + a[n-2] |
+ + | + |
a[n] = $($inits:expr),+ , ... , $recur:expr
+ ⌂ |
+ = 0, 1, ..., a[n-1] + a[n-2] |
+ + | + |
a[n] = $($inits:expr),+ , ... , $recur:expr
+ ⌂ |
+ 0, 1, ..., a[n-1] + a[n-2] |
+ + | + |
a[n] = $($inits:expr),+ , ... , $recur:expr
+ ⌂ |
+ 0, 1, ..., a[n-1] + a[n-2] |
+ + | + |
a[n] = $($inits:expr),+ , ... , $recur:expr
+ ⌂ ⌂ |
+ , 1, ..., a[n-1] + a[n-2] |
+ 0 |
+ + |
+ 注意: 这有两个 `⌂` ,因为下个输入标记既能匹配 重复元素间的分隔符逗号,也能匹配 标志重复结束的逗号。宏系统将同时追踪这两种可能,直到决定具体选择为止。 + | +|||
a[n] = $($inits:expr),+ , ... , $recur:expr
+ ⌂ ⌂ |
+ 1, ..., a[n-1] + a[n-2] |
+ 0 |
+ + |
a[n] = $($inits:expr),+ , ... , $recur:expr
+ |
+ , ..., a[n-1] + a[n-2] |
+ 0 , 1 |
+ + |
+ 注意:第一个被划掉的记号表明, + 基于上个被消耗的标记,宏系统排除了一项先前存在的可能。 + | +|||
a[n] = $($inits:expr),+ , ... , $recur:expr
+ ⌂ |
+ ..., a[n-1] + a[n-2] |
+ 0 , 1 |
+ + |
a[n] = $($inits:expr),+ , ... , $recur:expr
+ ⌂ |
+ , a[n-1] + a[n-2] |
+ 0 , 1 |
+ + |
a[n] = $($inits:expr),+ , ... , $recur:expr
+ ⌂ |
+ a[n-1] + a[n-2] |
+ 0 , 1 |
+ + |
a[n] = $($inits:expr),+ , ... , $recur:expr
+ ⌂ |
+ + | 0 , 1 |
+ a[n-1] + a[n-2] |
+
+ 注意:这一步表明,类似 $recur:expr + 的绑定将消耗一个完整的表达式。 + 究竟什么算是一个完整的表达式,将由编译器决定。 + 稍后我们会谈到语言其它部分的类似行为。 + | +
本章节将通过一个相对简单、实际的例子来介绍 Rust 的宏系统。 +我们不会试图解释整个宏系统错综复杂的构造; +而是试图让读者能够舒适地了解宏的书写方式,以及为何如斯。
+在 Rust Book 中也有专门一章 讲解宏 +(中文版), +同样提供了高层面的讲解。 +此外,本书也有一章 更富条理的介绍,旨在详细阐释宏系统。
+++译者注:建议初学者跟着文章思路一步步走下去, +从看懂文字说明和样例代码开始,能够运行的代码块运行一遍,看看效果。 +把样例代码复制出来跟着文章的说明依次更改。哪一步没跟上,就点右上角的展开按钮; +或者复制按钮,获取未隐藏的代码。
+
++注意:别慌!我们通篇只会涉及到下面这一点点数学。如果想直接看重点,本小节可被安全跳过。
+
所谓“递推 (recurrence) 关系”是指这样一个序列, +其中的每个值都由先前的一个或多个值决定, +并最终由一个或多个初始值完全决定。 +举例来说,Fibonacci 数列 +可被定义为如下关系:
+\[ F_{n} = 0, 1, ..., F_{n-1} + F_{n-2}\]
+即序列的前两个数分别为 0 和 1,而第 3 个则为 +\( F_{0} + F_{1} = 0 + 1 = 1\),第 4 个则为 \( F_{1} + F_{2} = 1 + 1 = 2\) +,依此类推。
+由于这列值可以永远持续下去,定义一个 fibonacci
的求值函数略显困难。
+显然,返回一整列值并不实际。
+我们真正需要的,应是某种具有惰求值性质的东西——只在必要的时候才进行运算求值。
在 Rust 中,这样的需求表明,是 Iterator
派上用场的时候了。
+实现迭代器并不十分困难,但比较繁琐:
+你得自定义一个类型,弄明白该在其中存储什么,然后为它实现 Iterator
trait。
其实,递推关系足够简单; +几乎所有的递推关系都可被抽象出来,变成一小段由宏驱动的代码生成机制。
+好了,说得已经足够多了,让我们开始干活吧。
+通常来说,在编写新宏时,我所做的第一件事,是决定宏调用的形式。 +在我们当前所讨论的情况下,我的初次尝试是这样:
+let fib = recurrence![a[n] = 0, 1, ..., a[n-1] + a[n-2]];
+
+for e in fib.take(10) { println!("{}", e) }
+以此为基点,我们可以向宏的定义迈出第一步, +即便在此时我们尚不了解该宏的展开部分究竟是什么样子。 +此步骤的用处在于,如果在此处无法明确如何解析输入语法, +那就可能意味着,整个宏的构思需要改变。
+macro_rules! recurrence {
+ ( a[n] = $($inits:expr),+ , ... , $recur:expr ) => { /* ... */ };
+}
+fn main() {}
+假设你并不熟悉相应的语法,让我来解释。
+上述代码块使用 macro_rules!
系统定义了一个宏,称为 recurrence!
。
+此宏仅包含一条解析规则,它规定,宏的输入必须依次匹配:
a
[
n
]
=
;$( ... )
),其内元素由,
分隔,允许重复一或多次( +
);
+重复的内容允许:
+inits
($inits:expr
),
...
,
;recur
($recur:expr
)。最后,规则表明,如果输入被成功匹配,则对该宏的调用将被标记序列 /* ... */
替换。
值得注意的是,inits
,如它命名采用的复数形式所暗示的,
+实际上包含所有成功匹配进此重复的表达式,而不仅是第一或最后一个。
+不仅如此,它们将被捕获成一个序列,而不是把它们不可逆地拼接在一起。
作为练习,我们将采用上面提及的输入,并研究它被处理的过程。
+由 ⌂
标出的“位置”将揭示下一个需要被匹配的句法模式。
+注意在某些情况下,下一个可用元素可能存在多个。
Input
表示所有尚未被消耗的标记。
+inits
和 recur
分别表示其对应绑定的内容。
Position | +Input | +inits |
+ recur |
+
---|---|---|---|
a[n] = $($inits:expr),+ , ... , $recur:expr
+ ⌂ |
+ a[n] = 0, 1, ..., a[n-1] + a[n-2] |
+ + | + |
a[n] = $($inits:expr),+ , ... , $recur:expr
+ ⌂ |
+ [n] = 0, 1, ..., a[n-1] + a[n-2] |
+ + | + |
a[n] = $($inits:expr),+ , ... , $recur:expr
+ ⌂ |
+ n] = 0, 1, ..., a[n-1] + a[n-2] |
+ + | + |
a[n] = $($inits:expr),+ , ... , $recur:expr
+ ⌂ |
+ ] = 0, 1, ..., a[n-1] + a[n-2] |
+ + | + |
a[n] = $($inits:expr),+ , ... , $recur:expr
+ ⌂ |
+ = 0, 1, ..., a[n-1] + a[n-2] |
+ + | + |
a[n] = $($inits:expr),+ , ... , $recur:expr
+ ⌂ |
+ 0, 1, ..., a[n-1] + a[n-2] |
+ + | + |
a[n] = $($inits:expr),+ , ... , $recur:expr
+ ⌂ |
+ 0, 1, ..., a[n-1] + a[n-2] |
+ + | + |
a[n] = $($inits:expr),+ , ... , $recur:expr
+ ⌂ ⌂ |
+ , 1, ..., a[n-1] + a[n-2] |
+ 0 |
+ + |
+ 注意: 这有两个 `⌂` ,因为下个输入标记既能匹配 重复元素间的分隔符逗号,也能匹配 标志重复结束的逗号。宏系统将同时追踪这两种可能,直到决定具体选择为止。 + | +|||
a[n] = $($inits:expr),+ , ... , $recur:expr
+ ⌂ ⌂ |
+ 1, ..., a[n-1] + a[n-2] |
+ 0 |
+ + |
a[n] = $($inits:expr),+ , ... , $recur:expr
+ |
+ , ..., a[n-1] + a[n-2] |
+ 0 , 1 |
+ + |
+ 注意:第一个被划掉的记号表明, + 基于上个被消耗的标记,宏系统排除了一项先前存在的可能。 + | +|||
a[n] = $($inits:expr),+ , ... , $recur:expr
+ ⌂ |
+ ..., a[n-1] + a[n-2] |
+ 0 , 1 |
+ + |
a[n] = $($inits:expr),+ , ... , $recur:expr
+ ⌂ |
+ , a[n-1] + a[n-2] |
+ 0 , 1 |
+ + |
a[n] = $($inits:expr),+ , ... , $recur:expr
+ ⌂ |
+ a[n-1] + a[n-2] |
+ 0 , 1 |
+ + |
a[n] = $($inits:expr),+ , ... , $recur:expr
+ ⌂ |
+ + | 0 , 1 |
+ a[n-1] + a[n-2] |
+
+ 注意:这一步表明,类似 $recur:expr + 的绑定将消耗一个完整的表达式。 + 究竟什么算是一个完整的表达式,将由编译器决定。 + 稍后我们会谈到语言其它部分的类似行为。 + | +
从此表中得到的最关键收获在于,宏系统会依次“尝试”将提供给它的每个标记当作输入, +然后与提供给它的每条规则进行匹配。我们稍后还将谈回到这一“尝试”。
+接下来我们开始写 宏调用完全展开后的形态。 +我们想要的结构类似:
+let fib = {
+ struct Recurrence {
+ mem: [u64; 2],
+ pos: usize,
+ }
+这就是我们实际使用的迭代器类型。
+其中, mem
负责存储最后计算得到的两个斐波那契值,
+以保证递推计算能够顺利进行; pos
则负责记录当前的 n
的值。
++附注:此处选用
+u64
是因为,对斐波那契数列来说,它已经“足够”了。 +先不必担心它是否适用于其它数列,我们会提到这一点的。
impl Iterator for Recurrence {
+ type Item = u64;
+
+ #[inline]
+ fn next(&mut self) -> Option<Self::Item> {
+ if self.pos < 2 {
+ let next_val = self.mem[self.pos];
+ self.pos += 1;
+ Some(next_val)
+我们需要这个 if
分支来返回序列的初始值,没什么技巧。
} else {
+ let a = /* something */;
+ let n = self.pos;
+ let next_val = a[n-1] + a[n-2];
+
+ self.mem.TODO_shuffle_down_and_append(next_val);
+
+ self.pos += 1;
+ Some(next_val)
+ }
+ }
+ }
+这段稍微难办一点。
+对于具体如何定义 a
,我们稍后再提。
+TODO_shuffle_down_and_append
的真面目也将留到稍后揭晓;
+我们想让它做到:将 next_val
放至数组末尾,
+并将数组中剩下的元素依次前移一格,最后丢掉原先的首元素。
+ Recurrence { mem: [0, 1], pos: 0 }
+};
+
+for e in fib.take(10) { println!("{}", e) }
+最后,我们返回一个该结构的实例。 +在随后的代码中,我们将用它来进行迭代。 +综上所述,完整的展开应该如下:
+let fib = {
+ struct Recurrence {
+ mem: [u64; 2],
+ pos: usize,
+ }
+
+ impl Iterator for Recurrence {
+ type Item = u64;
+
+ #[inline]
+ fn next(&mut self) -> Option<u64> {
+ if self.pos < 2 {
+ let next_val = self.mem[self.pos];
+ self.pos += 1;
+ Some(next_val)
+ } else {
+ let a = /* something */;
+ let n = self.pos;
+ let next_val = (a[n-1] + a[n-2]);
+
+ self.mem.TODO_shuffle_down_and_append(next_val.clone());
+
+ self.pos += 1;
+ Some(next_val)
+ }
+ }
+ }
+
+ Recurrence { mem: [0, 1], pos: 0 }
+};
+
+for e in fib.take(10) { println!("{}", e) }
+++附注:的确,这样做的确意味着每次调用该宏时,我们都会重新定义并实现一个
+Recurrence
结构。 +如果#[inline]
属性应用得当,在最终编译出的二进制文件中,大部分冗余都将被优化掉。
在写展开部分的过程中时常检查,也是一个有效的技巧。
+如果在过程中发现,展开中的某些内容需要根据调用的不同发生改变,
+但这些内容并未被我们的宏语法定义囊括;
+那就要去考虑,应该怎样去引入它们。
+在此示例中,我们先前用过一次 u64
,但调用者想要的类型不一定是它;
+然而我们的宏语法并没有提供其它选择。因此,我们可以做一些修改。
+macro_rules! recurrence { + ( a[n]: $sty:ty = $($inits:expr),+ , ... , $recur:expr ) => { /* ... */ }; +} + +/* +let fib = recurrence![a[n]: u64 = 0, 1, ..., a[n-1] + a[n-2]]; + +for e in fib.take(10) { println!("{}", e) } +*/ +fn main() {}
我们加入了一个新的 元变量 sty
,它应捕获一个类型 (type) 。
++附注:如果你不清楚在捕获冒号之后的部分,那可是几种语法匹配候选项之一。 +最常用的包括
+item
、expr
和ty
。 +完整的解释参考 元变量 。还要注意一点:为方便语言的未来发展,对于跟在某些特定的匹配之后的标记,编译器施加了一些限制。 +这种情况常在试图匹配至表达式 (expression) 或语句 (statement) 时出现: +它们后面仅允许跟进
+=>
、,
和;
这些标记之一。 +完整清单可在 +片段分类符的跟随限制 找到。
在此节中我们将略去一些实际上与宏的联系不太紧密的内容。
+这节的目标是,让用户可以通过索引 a
来访问数列中先前的值。
+a
应该如同一个滑动窗口 (sliding window),
+让我们得以持续访问数列中最近几个(在本例中,两个)值。
通过采用封装类型,我们可以轻易地做到这点:
+struct IndexOffset<'a> {
+ slice: &'a [u64; 2],
+ offset: usize,
+}
+
+impl<'a> Index<usize> for IndexOffset<'a> {
+ type Output = u64;
+
+ #[inline(always)]
+ fn index<'b>(&'b self, index: usize) -> &'b u64 {
+ use std::num::Wrapping;
+
+ let index = Wrapping(index);
+ let offset = Wrapping(self.offset);
+ let window = Wrapping(2);
+
+ let real_index = index - offset + window;
+ &self.slice[real_index.0]
+ }
+}
+++附注:对于新接触 Rust 的人来说,生命周期的概念经常需要一番思考。 +我们给出一些简单的解释:
+'a
和'b
是生命周期注解, +它们被用于追踪引用一直有效(引用:即一个指向某些数据的借用指针)。 +在此例中,IndexOffset
借用了一个指向迭代器数据的引用, +因此,它需要记录该引用能被保持有效的时长,记录的内容正是'a
。我们用到
+'b
,是因为Index::index
函数(下标句法正是通过此函数实现的) +的一个参数也需要生命周期。'a
和'b
不一定在所有情况下都相同。 +我们并没有显式地声明'a
和'b
之间有任何联系,但借用检查器 (borrow checker) +总会确保内存安全性不被意外破坏。
a
的定义将随之变为:
let a = IndexOffset { slice: &self.mem, offset: n };
+如何处理 TODO_shuffle_down_and_append
是我们现在剩下的唯一问题了。
+我没能在标准库中寻得可以直接使用的方法,但自己造一个出来并不难。
{
+ use std::mem::swap;
+
+ let mut swap_tmp = next_val;
+ for i in (0..2).rev() {
+ swap(&mut swap_tmp, &mut self.mem[i]);
+ }
+}
+它把新值替换至数组末尾,并把其他值向前移动一位。
+++附注:采用这种做法,将使得我们的代码可同时被用于不可拷贝 (non-copyable) 的类型。
+
至此,最终起作用的代码将是(可直接在代码块编辑,或者点击右上的运行按钮看看):
++macro_rules! recurrence { + ( a[n]: $sty:ty = $($inits:expr),+ , ... , $recur:expr ) => { /* ... */ }; +} + +fn main() { + /* + let fib = recurrence![a[n]: u64 = 0, 1, ..., a[n-1] + a[n-2]]; + + for e in fib.take(10) { println!("{}", e) } + */ + let fib = { + use std::ops::Index; + + struct Recurrence { + mem: [u64; 2], + pos: usize, + } + + struct IndexOffset<'a> { + slice: &'a [u64; 2], + offset: usize, + } + + impl<'a> Index<usize> for IndexOffset<'a> { + type Output = u64; + + #[inline(always)] + fn index<'b>(&'b self, index: usize) -> &'b u64 { + use std::num::Wrapping; + + let index = Wrapping(index); + let offset = Wrapping(self.offset); + let window = Wrapping(2); + + let real_index = index - offset + window; + &self.slice[real_index.0] + } + } + + impl Iterator for Recurrence { + type Item = u64; + + #[inline] + fn next(&mut self) -> Option<u64> { + if self.pos < 2 { + let next_val = self.mem[self.pos]; + self.pos += 1; + Some(next_val) + } else { + let next_val = { + let n = self.pos; + let a = IndexOffset { slice: &self.mem, offset: n }; + (a[n-1] + a[n-2]) + }; + + { + use std::mem::swap; + + let mut swap_tmp = next_val; + for i in (0..2).rev() { + swap(&mut swap_tmp, &mut self.mem[i]); + } + } + + self.pos += 1; + Some(next_val) + } + } + } + + Recurrence { mem: [0, 1], pos: 0 } + }; + + for e in fib.take(10) { println!("{}", e) } +}
注意我们改变了 n
与 a
的声明顺序,
+同时将它们(与递推表达式一起)用一个新区块包裹了起来。
+改变声明顺序的理由很明显,因为 n
得在 a
前被定义才能被 a
使用。
+而包裹的理由则是:如果不这么做,借用引用 &self.mem
会阻止随后的 swap
操作
+(在某物仍存在其它别名时,无法对其进行改变)。
+包裹区块将确保 &self.mem
产生的借用在彼时失效。
顺带一提,将交换 mem
的代码包进区块里的唯一原因,
+是为了缩减 std::mem::swap
的可用范畴,以保持代码整洁。
如果我们直接拿这段代码来跑,会顺利得到结果:
+0
+1
+1
+2
+3
+5
+8
+13
+21
+34
+
+现在,让我们把这段代码复制粘贴进宏的展开部分, +并把它们原本所在的位置换成一次宏调用。这样我们得到:
++macro_rules! recurrence { + ( a[n]: $sty:ty = $($inits:expr),+ , ... , $recur:expr ) => { + { + /* + What follows here is *literally* the code from before, + cut and pasted into a new position. No other changes + have been made. + */ + + use std::ops::Index; + + struct Recurrence { + mem: [u64; 2], + pos: usize, + } + + struct IndexOffset<'a> { + slice: &'a [u64; 2], + offset: usize, + } + + impl<'a> Index<usize> for IndexOffset<'a> { + type Output = u64; + + #[inline(always)] + fn index<'b>(&'b self, index: usize) -> &'b u64 { + use std::num::Wrapping; + + let index = Wrapping(index); + let offset = Wrapping(self.offset); + let window = Wrapping(2); + + let real_index = index - offset + window; + &self.slice[real_index.0] + } + } + + impl Iterator for Recurrence { + type Item = u64; + + #[inline] + fn next(&mut self) -> Option<u64> { + if self.pos < 2 { + let next_val = self.mem[self.pos]; + self.pos += 1; + Some(next_val) + } else { + let next_val = { + let n = self.pos; + let a = IndexOffset { slice: &self.mem, offset: n }; + (a[n-1] + a[n-2]) + }; + + { + use std::mem::swap; + + let mut swap_tmp = next_val; + for i in (0..2).rev() { + swap(&mut swap_tmp, &mut self.mem[i]); + } + } + + self.pos += 1; + Some(next_val) + } + } + } + + Recurrence { mem: [0, 1], pos: 0 } + } + }; +} + +fn main() { + let fib = recurrence![a[n]: u64 = 0, 1, ..., a[n-1] + a[n-2]]; + + for e in fib.take(10) { println!("{}", e) } +}
显然,宏的捕获尚未被用到,但这点很容易修改。
+不过,如果尝试编译上述代码,rustc
会中止,并显示:
error: local ambiguity: multiple parsing options: built-in NTs expr ('inits') or 1 other option.
+ --> src/main.rs:75:45
+ |
+75 | let fib = recurrence![a[n]: u64 = 0, 1, ..., a[n-1] + a[n-2]];
+ |
+
+这里我们撞上了 macro_rules!
的一处的跟随限制。
+问题出在那第二个逗号上。
+当在展开过程中遇见它时,编译器无法决定是该将它解析成 inits
中的又一个表达式,
+还是解析成 ...
。很遗憾,它不够聪明,没办法意识到 ...
不是一个有效的表达式,所以它选择了放弃。
+理论上 来说,上述代码应该能奏效,但当前它并不能。
++附注:有关宏系统如何解读我们的规则,我之前的确撒了点小谎(指没有从“正确”的规则开始)。 +通常来说,宏系统确实应当如我前述的那般运作,但在这里它没有。 +
+macro_rules!
的机制,由此看来,是存在一些小毛病的; +我们不得不记得偶尔去做一些微调,好让它我们期许的那般运作。在本例中,问题有两个。
++
+- 宏系统不清楚各式各样的语法元素(如表达式)可由什么样的东西构成, +或不能由什么样的东西构成;那是语法解析器的工作。
+- 在试图捕获复合语法元素(如表达式)的过程中,它如果不是 100% 地确定 +应该进行捕获的话,那么无法实行捕获。
+换句话说,宏系统可以向语法解析器发出请求,让解析器试图把某段输入当作表达式来进行解析; +但此间无论语法解析器遇见任何问题,都将中止整个进程以示回应。 +目前,宏系统处理这种窘境的唯一方式,就是对任何可能产生此类问题的情境加以禁止。
+好的一面在于,对于这摊子情况,没有任何人乐于看到。 +所以关键词
+macro
早已被预留,以备未来更加严密的宏系统使用。 +只是直到那天来临之前,我们还是该怎么做就怎么做,乖乖遵循跟随限制 :)
还好,修正方案也很简单:从宏句法中去掉逗号即可。
+出于平衡考量,我们将移除 ...
双边的逗号:
macro_rules! recurrence {
+ ( a[n]: $sty:ty = $($inits:expr),+ ... $recur:expr ) => { };
+}
+可惜作者在这里给的方案早在 1.14 版不再编译通过(由原版翻译者所言),
+而且至今(1.54 版,笔者所试)这里也无法使用 ...
编译,
+因为 expr
之后只能跟随 =>
、,
、;
之一
+以下续作者修改为编译通过的版本。
我们现在运气不好,因为我们想象出来的语法不会以这种方式工作,
+所以让我们只选择一个看起来最适合的。
+关键点在于分隔符不被识别,而通常使用 ,
或者 ;
作为分隔符,
+所以可以把原来的 , ... ,
替换成 ;
或者 ; ... ;
。
+macro_rules! recurrence { + ( a[n]: $sty:ty = $($inits:expr),+ ; ... ; $recur:expr ) => { +// ^~~~~~^ changed + /* ... */ + // Cheat :D + (vec![0u64, 1, 2, 3, 5, 8, 13, 21, 34]).into_iter() + }; +} + +fn main() { + let fib = recurrence![a[n]: u64 = 0, 1; ...; a[n-1] + a[n-2]]; +// ^~~~~^ changed + + for e in fib.take(10) { println!("{}", e) } +}
成功!现在,我们该将捕获部分捕获到的内容替代进展开部分中了。
+在宏中替换你捕获到的内容非常简单,
+通过 $sty:ty
捕获到的内容可用 $sty
来替换。
+好,让我们换掉那些 u64
吧:
+macro_rules! recurrence { + ( a[n]: $sty:ty = $($inits:expr),+ ; ... ; $recur:expr ) => { + { + use std::ops::Index; + + struct Recurrence { + mem: [$sty; 2], +// ^~~~ changed + pos: usize, + } + + struct IndexOffset<'a> { + slice: &'a [$sty; 2], +// ^~~~ changed + offset: usize, + } + + impl<'a> Index<usize> for IndexOffset<'a> { + type Output = $sty; +// ^~~~ changed + + #[inline(always)] + fn index<'b>(&'b self, index: usize) -> &'b $sty { +// ^~~~ changed + use std::num::Wrapping; + + let index = Wrapping(index); + let offset = Wrapping(self.offset); + let window = Wrapping(2); + + let real_index = index - offset + window; + &self.slice[real_index.0] + } + } + + impl Iterator for Recurrence { + type Item = $sty; +// ^~~~ changed + + #[inline] + fn next(&mut self) -> Option<$sty> { +// ^~~~ changed + /* ... */ + if self.pos < 2 { + let next_val = self.mem[self.pos]; + self.pos += 1; + Some(next_val) + } else { + let next_val = { + let n = self.pos; + let a = IndexOffset { slice: &self.mem, offset: n }; + (a[n-1] + a[n-2]) + }; + + { + use std::mem::swap; + + let mut swap_tmp = next_val; + for i in (0..2).rev() { + swap(&mut swap_tmp, &mut self.mem[i]); + } + } + + self.pos += 1; + Some(next_val) + } + } + } + + Recurrence { mem: [0, 1], pos: 0 } + } + }; +} + +fn main() { + let fib = recurrence![a[n]: u64 = 0, 1; ...; a[n-1] + a[n-2]]; + + for e in fib.take(10) { println!("{}", e) } +}
现在让我们来尝试更难的:如何将 inits
同时转变为字面值 [0, 1]
+以及数组类型 [$sty; 2]
。首先我们试试:
Recurrence { mem: [$($inits),+], pos: 0 }
+// ^~~~~~~~~~~ changed
+此段代码与捕获的效果正好相反:将 inits
捕得的内容排列开来,总共有 1 或多次,
+每条内容之间用逗号分隔。展开的结果与期望一致,我们得到标记序列:0, 1
。
不过,通过 inits
转换出字面值 2
需要一些技巧。
+没有直接可行的方法,但我们可以通过另一个宏做到。我们一步一步来。
+macro_rules! count_exprs { + /* ??? */ + () => {} +} +fn main() {}
先写显而易见的情况:未给表达式时,我们期望count_exprs
展开为字面值0
。
+macro_rules! count_exprs { + () => (0); +// ^~~~~~~~~~ added +} +fn main() { + const _0: usize = count_exprs!(); + assert_eq!(_0, 0); +}
++附注:你可能已经注意到了,这里的展开部分我用的是括号而非花括号。 +
+macro_rules!
其实不关心你用的是什么,只要它成对匹配即可:( )
、{ }
或[ ]
。 +实际上,宏本身的匹配符(即紧跟宏名称后的匹配符)、 +语法规则外的匹配符及相应展开部分外的匹配符都可以替换。调用宏时的括号也可被替换,但有些限制:当宏被以
+{...}
或(...);
形式调用时, +它总是会被解析为一个条目(item,比如struct
或fn
声明)。 +在函数体内部时,这一特征很重要,它将消除“解析成表达式”和“解析成语句”之间的歧义。
有一个表达式的情况该怎么办?应该展开为字面值 1
。
+macro_rules! count_exprs { + () => (0); + ($e:expr) => (1); +// ^~~~~~~~~~~~~~~~~ added +} +fn main() { + const _0: usize = count_exprs!(); + const _1: usize = count_exprs!(x); + assert_eq!(_0, 0); + assert_eq!(_1, 1); +}
两个呢?
++macro_rules! count_exprs { + () => (0); + ($e:expr) => (1); + ($e0:expr, $e1:expr) => (2); +// ^~~~~~~~~~~~~~~~~~~~~~~~~~~~ added +} +fn main() { + const _0: usize = count_exprs!(); + const _1: usize = count_exprs!(x); + const _2: usize = count_exprs!(x, y); + assert_eq!(_0, 0); + assert_eq!(_1, 1); + assert_eq!(_2, 2); +}
通过递归调用重新表达,我们可将扩展部分“精简”出来:
++macro_rules! count_exprs { + () => (0); + ($e:expr) => (1); + ($e0:expr, $e1:expr) => (1 + count_exprs!($e1)); +// ^~~~~~~~~~~~~~~~~~~~~ changed +} +fn main() { + const _0: usize = count_exprs!(); + const _1: usize = count_exprs!(x); + const _2: usize = count_exprs!(x, y); + assert_eq!(_0, 0); + assert_eq!(_1, 1); + assert_eq!(_2, 2); +}
这样做可行是因为,Rust可将 1 + 1
合并成一个常量。
+那么,三种表达式的情况呢?
+macro_rules! count_exprs { + () => (0); + ($e:expr) => (1); + ($e0:expr, $e1:expr) => (1 + count_exprs!($e1)); + ($e0:expr, $e1:expr, $e2:expr) => (1 + count_exprs!($e1, $e2)); +// ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ added +} +fn main() { + const _0: usize = count_exprs!(); + const _1: usize = count_exprs!(x); + const _2: usize = count_exprs!(x, y); + const _3: usize = count_exprs!(x, y, z); + assert_eq!(_0, 0); + assert_eq!(_1, 1); + assert_eq!(_2, 2); + assert_eq!(_3, 3); +}
++附注:你可能会想,我们是否能翻转这些规则的排列顺序。 +在此情境下,可以。但在有些情况下,宏系统可能会对此挑剔。 +如果你发现自己有一个包含多项规则的宏系统老是报错,或给出期望外的结果; +但你发誓它应该能用,试着调换一下规则的排序吧。
+
我们希望你现在已经能看出规律。
+通过匹配至一个表达式加上 0 或多个表达式并展开成 1+a
,我们可以减少规则列表的数目:
+macro_rules! count_exprs { + () => (0); + ($head:expr) => (1); + ($head:expr, $($tail:expr),*) => (1 + count_exprs!($($tail),*)); +// ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ changed +} +fn main() { + const _0: usize = count_exprs!(); + const _1: usize = count_exprs!(x); + const _2: usize = count_exprs!(x, y); + const _3: usize = count_exprs!(x, y, z); + assert_eq!(_0, 0); + assert_eq!(_1, 1); + assert_eq!(_2, 2); + assert_eq!(_3, 3); +}
++仅对此例: +这段代码并非计数仅有或其最好的方法。 +若有兴趣,稍后可以研读 计数 一节。
+
有此工具后,我们可再次修改 recurrence
,确定 mem
所需的大小。
+// added: +macro_rules! count_exprs { + () => (0); + ($head:expr) => (1); + ($head:expr, $($tail:expr),*) => (1 + count_exprs!($($tail),*)); +} + +macro_rules! recurrence { + ( a[n]: $sty:ty = $($inits:expr),+ ; ... ; $recur:expr ) => { + { + use std::ops::Index; + + const MEM_SIZE: usize = count_exprs!($($inits),+); +// ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ added + + struct Recurrence { + mem: [$sty; MEM_SIZE], +// ^~~~~~~~ changed + pos: usize, + } + + struct IndexOffset<'a> { + slice: &'a [$sty; MEM_SIZE], +// ^~~~~~~~ changed + offset: usize, + } + + impl<'a> Index<usize> for IndexOffset<'a> { + type Output = $sty; + + #[inline(always)] + fn index<'b>(&'b self, index: usize) -> &'b $sty { + use std::num::Wrapping; + + let index = Wrapping(index); + let offset = Wrapping(self.offset); + let window = Wrapping(MEM_SIZE); +// ^~~~~~~~ changed + + let real_index = index - offset + window; + &self.slice[real_index.0] + } + } + + impl Iterator for Recurrence { + type Item = $sty; + + #[inline] + fn next(&mut self) -> Option<$sty> { + if self.pos < MEM_SIZE { +// ^~~~~~~~ changed + let next_val = self.mem[self.pos]; + self.pos += 1; + Some(next_val) + } else { + let next_val = { + let n = self.pos; + let a = IndexOffset { slice: &self.mem, offset: n }; + (a[n-1] + a[n-2]) + }; + + { + use std::mem::swap; + + let mut swap_tmp = next_val; + for i in (0..MEM_SIZE).rev() { +// ^~~~~~~~ changed + swap(&mut swap_tmp, &mut self.mem[i]); + } + } + + self.pos += 1; + Some(next_val) + } + } + } + + Recurrence { mem: [$($inits),+], pos: 0 } + } + }; +} +/* ... */ + +fn main() { + let fib = recurrence![a[n]: u64 = 0, 1; ...; a[n-1] + a[n-2]]; + + for e in fib.take(10) { println!("{}", e) } +}
完成之后,我们开始替换最后的 recur
表达式。
+macro_rules! count_exprs { + () => (0); + ($head:expr $(, $tail:expr)*) => (1 + count_exprs!($($tail),*)); +} +macro_rules! recurrence { + ( a[n]: $sty:ty = $($inits:expr),+ ; ... ; $recur:expr ) => { + { + use std::ops::Index; + + const MEM_SIZE: usize = count_exprs!($($inits),+); + struct Recurrence { + mem: [$sty; MEM_SIZE], + pos: usize, + } + struct IndexOffset<'a> { + slice: &'a [$sty; MEM_SIZE], + offset: usize, + } + impl<'a> Index<usize> for IndexOffset<'a> { + type Output = $sty; + + #[inline(always)] + fn index<'b>(&'b self, index: usize) -> &'b $sty { + use std::num::Wrapping; + + let index = Wrapping(index); + let offset = Wrapping(self.offset); + let window = Wrapping(MEM_SIZE); + + let real_index = index - offset + window; + &self.slice[real_index.0] + } + } + impl Iterator for Recurrence { + type Item = $sty; +/* ... */ + #[inline] + fn next(&mut self) -> Option<u64> { + if self.pos < MEM_SIZE { + let next_val = self.mem[self.pos]; + self.pos += 1; + Some(next_val) + } else { + let next_val = { + let n = self.pos; + let a = IndexOffset { slice: &self.mem, offset: n }; + $recur +// ^~~~~~ changed + }; + { + use std::mem::swap; + let mut swap_tmp = next_val; + for i in (0..MEM_SIZE).rev() { + swap(&mut swap_tmp, &mut self.mem[i]); + } + } + self.pos += 1; + Some(next_val) + } + } +/* ... */ + } + Recurrence { mem: [$($inits),+], pos: 0 } + } + }; +} +fn main() { + let fib = recurrence![a[n]: u64 = 1, 1; ...; a[n-1] + a[n-2]]; + for e in fib.take(10) { println!("{}", e) } +}
现在试图编译的话...
+error[E0425]: cannot find value `a` in this scope
+ --> src/main.rs:68:50
+ |
+68 | let fib = recurrence![a[n]: u64 = 1, 1; ...; a[n-1] + a[n-2]];
+ | ^ not found in this scope
+
+error[E0425]: cannot find value `n` in this scope
+ --> src/main.rs:68:52
+ |
+68 | let fib = recurrence![a[n]: u64 = 1, 1; ...; a[n-1] + a[n-2]];
+ | ^ not found in this scope
+
+error[E0425]: cannot find value `a` in this scope
+ --> src/main.rs:68:59
+ |
+68 | let fib = recurrence![a[n]: u64 = 1, 1; ...; a[n-1] + a[n-2]];
+ | ^ not found in this scope
+
+error[E0425]: cannot find value `n` in this scope
+ --> src/main.rs:68:61
+ |
+68 | let fib = recurrence![a[n]: u64 = 1, 1; ...; a[n-1] + a[n-2]];
+ | ^ not found in this scope
+
+...等等,什么情况?这没道理...让我们看看宏究竟展开成了什么样子。
+$ rustc -Z unstable-options --pretty expanded recurrence.rs
+
+参数 --pretty expanded
将促使 rustc
展开宏,并将输出的 AST 再重转为源代码。
+此选项当前被认定为是 unstable
,因此我们还要添加 -Z unstable-options
。
+输出的信息(经过整理格式后)如下;特别留意 $recur
被替换掉的位置:
#![feature(no_std)]
+#![no_std]
+#[prelude_import]
+use std::prelude::v1::*;
+#[macro_use]
+extern crate std as std;
+fn main() {
+ let fib = {
+ use std::ops::Index;
+ const MEM_SIZE: usize = 1 + 1;
+ struct Recurrence {
+ mem: [u64; MEM_SIZE],
+ pos: usize,
+ }
+ struct IndexOffset<'a> {
+ slice: &'a [u64; MEM_SIZE],
+ offset: usize,
+ }
+ impl <'a> Index<usize> for IndexOffset<'a> {
+ type Output = u64;
+ #[inline(always)]
+ fn index<'b>(&'b self, index: usize) -> &'b u64 {
+ use std::num::Wrapping;
+ let index = Wrapping(index);
+ let offset = Wrapping(self.offset);
+ let window = Wrapping(MEM_SIZE);
+ let real_index = index - offset + window;
+ &self.slice[real_index.0]
+ }
+ }
+ impl Iterator for Recurrence {
+ type Item = u64;
+ #[inline]
+ fn next(&mut self) -> Option<u64> {
+ if self.pos < MEM_SIZE {
+ let next_val = self.mem[self.pos];
+ self.pos += 1;
+ Some(next_val)
+ } else {
+ let next_val = {
+ let n = self.pos;
+ let a = IndexOffset{slice: &self.mem, offset: n,};
+ a[n - 1] + a[n - 2]
+ };
+ {
+ use std::mem::swap;
+ let mut swap_tmp = next_val;
+ {
+ let result =
+ match ::std::iter::IntoIterator::into_iter((0..MEM_SIZE).rev()) {
+ mut iter => loop {
+ match ::std::iter::Iterator::next(&mut iter) {
+ ::std::option::Option::Some(i) => {
+ swap(&mut swap_tmp, &mut self.mem[i]);
+ }
+ ::std::option::Option::None => break,
+ }
+ },
+ };
+ result
+ }
+ }
+ self.pos += 1;
+ Some(next_val)
+ }
+ }
+ }
+ Recurrence{mem: [0, 1], pos: 0,}
+ };
+ {
+ let result =
+ match ::std::iter::IntoIterator::into_iter(fib.take(10)) {
+ mut iter => loop {
+ match ::std::iter::Iterator::next(&mut iter) {
+ ::std::option::Option::Some(e) => {
+ ::std::io::_print(::std::fmt::Arguments::new_v1(
+ {
+ static __STATIC_FMTSTR: &'static [&'static str] = &["", "\n"];
+ __STATIC_FMTSTR
+ },
+ &match (&e,) {
+ (__arg0,) => [::std::fmt::ArgumentV1::new(__arg0, ::std::fmt::Display::fmt)],
+ }
+ ))
+ }
+ ::std::option::Option::None => break,
+ }
+ },
+ };
+ result
+ }
+}
+呃..这看起来完全合法!
+如果我们加上几条 #![feature(...)]
属性,并把它送去给一个 nightly 版本的 rustc
,
+甚至真能通过编译...究竟什么情况?!
++附注:上述代码无法通过非 nightly 版
+rustc
编译。 +这是因为,println!
宏的展开结果依赖于编译器内部的细节,这些细节尚未被公开稳定化。
这儿的问题在于,Rust 宏中的标识符具有卫生性。 +这就是说,出自不同上下文的标识符不可能发生冲突。 +作为演示,举个简单的例子。
+macro_rules! using_a {
+ ($e:expr) => {
+ {
+ let a = 42i;
+ $e
+ }
+ }
+}
+
+let four = using_a!(a / 10);
+fn main() {}
+此宏接受一个表达式,然后把它包进一个定义了变量 a
的代码块里。
+我们随后用它绕个弯子来求 4
。
+这个例子中实际上存在 2 种句法上下文,但我们看不见它们。
+为了帮助说明,我们给每个上下文都上一种不同的颜色。
+我们从未展开的代码开始上色,此时仅看得见一种上下文:
macro_rules! using_a {
($e:expr) => {
{
let a = 42;
$e
}
}
}
let four = using_a!(a / 10);
+现在,展开宏调用。
+let four = { let a = 42; a / 10 };+
可以看到,在宏中定义的a
+与调用所提供的a
处于不同的上下文中。
+因此,虽然它们的字母表示一致,编译器仍将它们视作完全不同的标识符。
宏的这一特性需要格外留意:它们可能会产出无法通过编译的 AST;
+但同样的代码,手写或通过 --pretty expanded
转印出来则能够通过编译。
解决方案是,采用合适的句法上下文来捕获标识符。我们沿用上例,并作修改:
+macro_rules! using_a {
($a:ident, $e:expr) => {
{
let $a = 42;
$e
}
}
}
let four = using_a!(a, a / 10);
+现在它将展开为:
+let four = { let a = 42; a / 10 };+
上下文现在匹配了,编译通过。
+我们的 recurrence!
宏也可被如此调整:
+显式地捕获a
与n
即可。调整后我们得到:
+macro_rules! count_exprs { + () => (0); + ($head:expr) => (1); + ($head:expr, $($tail:expr),*) => (1 + count_exprs!($($tail),*)); +} + +macro_rules! recurrence { + ( $seq:ident [ $ind:ident ]: $sty:ty = $($inits:expr),+ ; ... ; $recur:expr ) => { +// ^~~~~~~~~~ ^~~~~~~~~~ changed + { + use std::ops::Index; + + const MEM_SIZE: usize = count_exprs!($($inits),+); + + struct Recurrence { + mem: [$sty; MEM_SIZE], + pos: usize, + } + + struct IndexOffset<'a> { + slice: &'a [$sty; MEM_SIZE], + offset: usize, + } + + impl<'a> Index<usize> for IndexOffset<'a> { + type Output = $sty; + + #[inline(always)] + fn index<'b>(&'b self, index: usize) -> &'b $sty { + use std::num::Wrapping; + + let index = Wrapping(index); + let offset = Wrapping(self.offset); + let window = Wrapping(MEM_SIZE); + + let real_index = index - offset + window; + &self.slice[real_index.0] + } + } + + impl Iterator for Recurrence { + type Item = $sty; + + #[inline] + fn next(&mut self) -> Option<$sty> { + if self.pos < MEM_SIZE { + let next_val = self.mem[self.pos]; + self.pos += 1; + Some(next_val) + } else { + let next_val = { + let $ind = self.pos; +// ^~~~ changed + let $seq = IndexOffset { slice: &self.mem, offset: $ind }; +// ^~~~ changed + $recur + }; + + { + use std::mem::swap; + + let mut swap_tmp = next_val; + for i in (0..MEM_SIZE).rev() { + swap(&mut swap_tmp, &mut self.mem[i]); + } + } + + self.pos += 1; + Some(next_val) + } + } + } + + Recurrence { mem: [$($inits),+], pos: 0 } + } + }; +} + +fn main() { + let fib = recurrence![a[n]: u64 = 0, 1; ...; a[n-1] + a[n-2]]; + + for e in fib.take(10) { println!("{}", e) } +}
通过编译了!接下来,我们试试别的数列。
++macro_rules! count_exprs { + () => (0); + ($head:expr) => (1); + ($head:expr, $($tail:expr),*) => (1 + count_exprs!($($tail),*)); +} + +macro_rules! recurrence { + ( $seq:ident [ $ind:ident ]: $sty:ty = $($inits:expr),+ ; ... ; $recur:expr ) => { + { + use std::ops::Index; + + const MEM_SIZE: usize = count_exprs!($($inits),+); + + struct Recurrence { + mem: [$sty; MEM_SIZE], + pos: usize, + } + + struct IndexOffset<'a> { + slice: &'a [$sty; MEM_SIZE], + offset: usize, + } + + impl<'a> Index<usize> for IndexOffset<'a> { + type Output = $sty; + + #[inline(always)] + fn index<'b>(&'b self, index: usize) -> &'b $sty { + use std::num::Wrapping; + + let index = Wrapping(index); + let offset = Wrapping(self.offset); + let window = Wrapping(MEM_SIZE); + + let real_index = index - offset + window; + &self.slice[real_index.0] + } + } + + impl Iterator for Recurrence { + type Item = $sty; + + #[inline] + fn next(&mut self) -> Option<$sty> { + if self.pos < MEM_SIZE { + let next_val = self.mem[self.pos]; + self.pos += 1; + Some(next_val) + } else { + let next_val = { + let $ind = self.pos; + let $seq = IndexOffset { slice: &self.mem, offset: $ind }; + $recur + }; + + { + use std::mem::swap; + + let mut swap_tmp = next_val; + for i in (0..MEM_SIZE).rev() { + swap(&mut swap_tmp, &mut self.mem[i]); + } + } + + self.pos += 1; + Some(next_val) + } + } + } + + Recurrence { mem: [$($inits),+], pos: 0 } + } + }; +} + +fn main() { +for e in recurrence!(f[i]: f64 = 1.0; ...; f[i-1] * i as f64).take(10) { + println!("{}", e) +} +}
运行上述代码得到:
+1
+1
+2
+6
+24
+120
+720
+5040
+40320
+362880
+
+成功!
+++译者注:导出宏 这部分内容由译者所补充。
+这个例子的代码是放在 bin crate 中运行的,如果把 宏 的代码放在 lib crate, +
+main
函数放在 bin crate,那么需要做一点更改。(即使它们都处于同一个 package 下)
假设 recurrence!
和其依赖的 count_exprs!
被定义在 macs
lib crate,
+根据 导入/导出宏#2018 版本 小节。
首先你得把这两个宏导出,否则使用 macs
lib 的 crate 会找不到宏:
#[macro_export]
+macro_rules! count_exprs { /* */ }
+
+#[macro_export]
+macro_rules! recurrence { /* */ }
+
+其次,你需要在 recurrence!
中引入 count_exprs!
,使用:
// --snippet--
+
+#[macro_export]
+macro_rules! recurrence {
+ ( $seq:ident [ $ind:ident ]: $sty:ty = $($inits:expr),+ ; ... ; $recur:expr ) => {
+ {
+ use std::ops::Index;
+ use $crate::count_exprs; // 导入
+
+ const MEM_SIZE: usize = count_exprs!($($inits),+);
+
+// --snippet--
+
+
+ ++RFC: rfcs#1584
+
+Tracking Issue: rust#39412
+Feature:#![feature(decl_macro)]
虽然这还未稳定(或者更确切地说,还远未完成),但有人提议建立一个新的声明宏系统,该系统应该取代
+macro_rules!
,并给其取名为声明宏 2.0、macro
、decl_macro
或者更混乱的名称 macros-by-example
。
本章只是为了快速浏览当前状态,展示如何使用这个宏系统以及它的不同之处。
+这里所描述的一切都不是最终成型的或完整的,因为它们可能会发生变化。
+我们将对前几章中实现的两个宏在 macro
和 macro_rules
的语法之间进行比较:
+#![feature(decl_macro)] + +macro_rules! replace_expr_ { + ($_t:tt $sub:expr) => { $sub } +} +macro replace_expr($_t:tt $sub:expr) { + $sub +} + +macro_rules! count_tts_ { + () => { 0 }; + ($odd:tt $($a:tt $b:tt)*) => { (count_tts!($($a)*) << 1) | 1 }; + ($($a:tt $even:tt)*) => { count_tts!($($a)*) << 1 }; +} +macro count_tts { + () => { 0 }, + ($odd:tt $($a:tt $b:tt)*) => { (count_tts!($($a)*) << 1) | 1 }, + ($($a:tt $even:tt)*) => { count_tts!($($a)*) << 1 }, +} + +fn main() {}
它们看起来非常相似,只是有一些不同之处,而且 macro
有两种不同的形式。
让我们先看 count_tts
宏,因为它看起来更像我们习惯看到的样子。虽然它看起来与
+macro_rules
的版本几乎相同,但有两个不同之处:
macro
关键字,
而不是 ;
不过,macro
还有另一种形式,这是只有一条规则的宏的简写。通过
+replace_expr
,我们看到,可以用一种更类似于普通函数的方式来编写定义:
=>
,再写 transcriber调用 macro
所定义的宏,和函数式宏的语法相同,名称后跟 !
,再后跟宏输入标记树。
macro
是规范的条目macro_rules
宏是按文本限定范围的,并且如果将它视为条目,需要 #[macro_export]
+(而且还可能需要重导出),但 macro
与此不同,因为 macro
宏的行为与规范的条目一样。
因此,你可以使用诸如 pub
、pub(crate)
、pub(in path)
之类的可见性分类符来适当地限定它们。1
译者注:这也意味着,macro
宏的导入导出规则符合常规条目。
到目前为止,卫生性是这两个声明宏系统之间最大的区别。
+与具有混合式卫生性 (mixed site hygiene) 的 macro_rules
+不同,macro
具有定义处卫生性 (definition site hygiene),这意味着
+macro
不会将标识符泄漏到其调用之外。
这样,下面的代码可以使用 macro_rules
宏进行编译,但无法使用 macro
定义:
+#![feature(decl_macro)] +// 试着注释下面第一行,然后取消注释下面第二行,看看会发生什么 + +macro_rules! foo { +// macro foo { + ($name: ident) => { + pub struct $name; + + impl $name { + pub fn new() -> $name { + $name + } + } + } +} + +foo!(Foo); + +fn main() { + // this fails with a `macro`, but succeeds with a `macro_rules` + let foo = Foo::new(); +}
未来可能会有计划允许标识符卫生性逃逸 (hygiene bending)。
+ +本节将介绍 macro_rules!
宏系统的一些细枝末节。你至少应该 记住 有这些东西存在。
++注意:这是一个专门为声明宏量身定做的调试工具清单,调试宏的其他方法可以在语法扩展的 +调试 章节中找到。
+
trace_macros!
最有用的是 trace_macros!
,在每次声明宏展开前,它指示编译器记录下声明宏的调用信息。
例如:
+// 注意:这需要 nightly Rust
+#![feature(trace_macros)]
+
+macro_rules! each_tt {
+ () => {};
+ ($_tt:tt $($rest:tt)*) => {each_tt!($($rest)*);};
+}
+
+each_tt!(foo bar baz quux);
+trace_macros!(true);
+each_tt!(spim wak plee whum);
+trace_macros!(false);
+each_tt!(trom qlip winp xod);
+
+fn main() {}
+输出为:
+note: trace_macro
+ --> src/main.rs:11:1
+ |
+11 | each_tt!(spim wak plee whum);
+ | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+ |
+ = note: expanding `each_tt! { spim wak plee whum }`
+ = note: to `each_tt ! (wak plee whum) ;`
+ = note: expanding `each_tt! { wak plee whum }`
+ = note: to `each_tt ! (plee whum) ;`
+ = note: expanding `each_tt! { plee whum }`
+ = note: to `each_tt ! (whum) ;`
+ = note: expanding `each_tt! { whum }`
+ = note: to `each_tt ! () ;`
+ = note: expanding `each_tt! { }`
+ = note: to ``
+
+它在调试递归很深的宏时尤其有用。
+此外,你可以在命令行里,给编译指令附加 -Z trace-macros
来打印追踪的宏。
trace_macros!(false);
之后的宏不会被这个附加指令追踪到,所以这里会追踪前两个宏。
参考命令:cargo rustc --bin binary_name -- -Z trace-macros
log_syntax!
另一有用的宏是 log_syntax!
。它将使得编译器输出所有经过编译器处理的标记。
比如让编译器“唱首歌”:
++// 注意:这需要 nightly Rust +#![feature(log_syntax)] + +macro_rules! sing { + () => {}; + ($tt:tt $($rest:tt)*) => {log_syntax!($tt); sing!($($rest)*);}; +} + +sing! { + ^ < @ < . @ * + '\x08' '{' '"' _ # ' ' + - @ '$' && / _ % + ! ( '\t' @ | = > + ; '\x08' '\'' + '$' ? '\x7f' + , # '"' ~ | ) '\x07' +} + +fn main() {}
比起 trace_macros!
来说,它能够做一些更有针对性的调试。
macro_railroad
lib另一个很棒的工具是 lukaslueg 编写的 macro_railroad
lib。
它能可视化地生成 Rust macro_rules!
宏的语法图 (syntax diagrams)。
正如在 思路 一章看到的,截至 1.60 版本, Rust 已有 +14 个片段分类符 (Fragment Specifiers,以下简称分类符)1。
+这一节会更深入地探讨他们之中的细节,每次都会展示几个匹配的例子。
+++ +注意:除了
+ident
、lifetime
和tt
分类符之外,其余的分类符在匹配后生成的 +AST 是不清楚的 (opaque),这使得在之后的宏调用时不可能检查 (inspect) 捕获的结果。2
最新内容可参考 Reference 的 Metavariables 一节。
+推荐通过 rust quiz #9 来理解这句话。
+block
block
分类符只匹配
+block 表达式。
块 (block) 由 {
开始,接着是一些语句,最后是可选的表达式,然后以 }
结束。
+块的类型要么是最后的值表达式类型,要么是 ()
类型。
+macro_rules! blocks { + ($($block:block)*) => (); +} + +blocks! { + {} + { + let zig; + } + { 2 } +} +fn main() {}
expr
expr
分类符用于匹配任何形式的表达式
+(expression)。
(如果把 Rust 视为面向表达式的语言,那么它有很多种表达式。)
++macro_rules! expressions { + ($($expr:expr)*) => (); +} + +expressions! { + "literal" + funcall() + future.await + break 'foo bar +} +fn main() {}
ident
ident
分类符用于匹配任何形式的标识符
+(identifier) 或者关键字。
+。
+macro_rules! idents { + ($($ident:ident)*) => (); +} + +idents! { + // _ <- `_` 不是标识符,而是一种模式 + foo + async + O_________O + _____O_____ +} +fn main() {}
item
item
分类符只匹配 Rust 的 item
+的 定义 (definitions) ,
+不会匹配指向 item 的标识符 (identifiers)。例子:
+macro_rules! items { + ($($item:item)*) => (); +} + +items! { + struct Foo; + enum Bar { + Baz + } + impl Foo {} + /*...*/ +} +fn main() {}
item
+是在编译时完全确定的,通常在程序执行期间保持固定,并且可以驻留在只读存储器中。具体指:
extern crate
declarationsuse
declarationsextern
blockslifetime
lifetime
分类符用于匹配生命周期注解或者标签
+(lifetime or label)。
+它与 ident
很像,但是 lifetime
会匹配到前缀 ''
。
+macro_rules! lifetimes { + ($($lifetime:lifetime)*) => (); +} + +lifetimes! { + 'static + 'shiv + '_ +} +fn main() {}
literal
literal
分类符用于匹配字面表达式
+(literal expression)。
+macro_rules! literals { + ($($literal:literal)*) => (); +} + +literals! { + -1 + "hello world" + 2.3 + b'b' + true +} +fn main() {}
meta
meta
分类符用于匹配属性 (attribute),
+准确地说是属性里面的内容。通常你会在 #[$meta:meta]
或 #![$meta:meta]
模式匹配中
+看到这个分类符。
+macro_rules! metas { + ($($meta:meta)*) => (); +} + +metas! { + ASimplePath + super::man + path = "home" + foo(bar) +} +fn main() {}
++针对文档注释简单说一句: +文档注释其实是具有
+#[doc="…"]
形式的属性,...
实际上就是注释字符串, +这意味着你可以在在宏里面操作文档注释!
pat
pat
分类符用于匹配任何形式的模式
+(pattern),包括 2021 edition
+开始的 or-patterns。
+macro_rules! patterns { + ($($pat:pat)*) => (); +} + +patterns! { + "literal" + _ + 0..5 + ref mut PatternsAreNice + 0 | 1 | 2 | 3 +} +fn main() {}
pat_param
从 2021 edition 起, or-patterns 模式开始应用,这让 pat
分类符不再允许跟随 |
。
为了避免这个问题或者说恢复旧的 pat
分类符行为,你可以使用 pat_param
片段,它允许
+|
跟在它后面,因为 pat_param
不允许 top level 或 or-patterns。
+macro_rules! patterns { + (pat: $pat:pat) => { + println!("pat: {}", stringify!($pat)); + }; + (pat_param: $($pat:pat_param)|+) => { + $( println!("pat_param: {}", stringify!($pat)); )+ + }; +} +fn main() { + patterns! { + pat: 0 | 1 | 2 | 3 + } + patterns! { + pat_param: 0 | 1 | 2 | 3 + } +}
+macro_rules! patterns { + ($( $( $pat:pat_param )|+ )*) => (); +} + +patterns! { + "literal" + _ + 0..5 + ref mut PatternsAreNice + 0 | 1 | 2 | 3 +} +fn main() {}
path
path
分类符用于匹配类型中的路径
+(TypePath)。
这包括函数式的 trait 形式。
++macro_rules! paths { + ($($path:path)*) => (); +} + +paths! { + ASimplePath + ::A::B::C::D + G::<eneri>::C + FnMut(u32) -> () +} +fn main() {}
stmt
stmt
分类符只匹配的 语句 (statement)。
+除非 item 语句要求结尾有分号,否则 不会 匹配语句最后的分号。
什么叫 item 语句要求结尾有分号呢?单元结构体 (Unit-Struct) 就是一个简单的例子, +因为它定义中必须带上结尾的分号。
+赶紧用例子展示上面说的是啥意思吧。下面的宏只给出它所捕获的内容,因为有几行不能通过编译。
++macro_rules! statements { + ($($stmt:stmt)*) => ($($stmt)*); +} + +fn main() { + statements! { + struct Foo; + fn foo() {} + let zig = 3 + let zig = 3; + 3 + 3; + if true {} else {} + {} + } +}
你可以根据报错内容试着删除不能编译的代码,结合 stmt
小节开头的文字再琢磨琢磨。
+你如果正浏览使用 mdbook 渲染的页面,那么可以直接运行和修改这段代码。
虽然源代码编译失败,但是我们可以展开宏3,
+使用 playground 的
+Expand macros
工具 (tool);或者把代码复制到本地,在 nightly Rust 版本中使用
+cargo rustc -- -Zunstable-options --pretty=expanded
命令得到宏展开结果:
warning: unnecessary trailing semicolon
+ --> src/main.rs:10:20
+ |
+10 | let zig = 3;
+ | ^ help: remove this semicolon
+ |
+ = note: `#[warn(redundant_semicolons)]` on by default
+
+warning: unnecessary trailing semicolon
+ --> src/main.rs:12:10
+ |
+12 | 3;
+ | ^ help: remove this semicolon
+
+#![feature(prelude_import)]
+#[prelude_import]
+use std::prelude::rust_2018::*;
+#[macro_use]
+extern crate std;
+macro_rules! statements { ($ ($ stmt : stmt) *) => ($ ($ stmt) *) ; }
+
+fn main() {
+ struct Foo;
+ fn foo() { }
+ let zig = 3;
+ let zig = 3;
+ ;
+ 3;
+ 3;
+ ;
+ if true { } else { }
+ { }
+}
+由此我们知道:
+虽然 stmt
分类符没有捕获语句末尾的分号,但它依然在所需的时候返回了 (emit)
+语句。原因很简单,分号本身就是有效的语句。所以我们实际输入 10 个语句调用了宏,而不是 8
+个!这在把多个反复捕获放入一个反复展开时很重要,因为此时反复的次数必须相同。
在这里你应该注意到:struct Foo;
被匹配到了。否则我们会看到像其他情况一样有一个额外 ;
+语句。由前所述,这能想通:item 语句需要分号,所以这个分号能被匹配到。
仅由块表达式或控制流表达式组成的表达式结尾没有分号, +其余的表达式捕获后产生的表达式会尾随一个分号(在这个例子中,正是这里出错)。
+这里提到的细节能在 Reference 的 statement +一节中找到。但个细节通常这并不重要,除了要注意反复次数,通常没什么问题。
+可阅读 调试 一章
+tt
tt
分类符用于匹配标记树 (TokenTree)。
+如果你是新手,对标记树不了解,那么需要回顾本书
+标记树
+一节。tt
分类符是最有作用的分类符之一,因为它能匹配几乎所有东西,
+而且能够让你在使用宏之后检查 (inspect) 匹配的内容。
这让你可以编写非常强大的宏技巧,比如 +tt-muncher 和 +push-down-accumulator。
+ty
ty
分类符用于匹配任何形式的类型表达式 (type expression)。
类型表达式是在 Rust 中指代类型的语法。
++macro_rules! types { + ($($type:ty)*) => (); +} + +types! { + foo::bar + bool + [u8] + impl IntoIterator<Item = u32> +} +fn main() {}
vis
vis
分类符会匹配 可能为空 可见性修饰符 (Visibility qualifier)。
+macro_rules! visibilities { + // ∨~~注意这个逗号,`vis` 分类符自身不会匹配到逗号 + ($($vis:vis,)*) => (); +} + +visibilities! { + , // 没有 vis 也行,因为 $vis 隐式包含 `?` 的情况 + pub, + pub(crate), + pub(in super), + pub(in some_path), +} +fn main() {}
vis
实际上只支持例子里的几种方式,因为这里的 visibility
+指的是可见性,与私有性相对。而涉及这方面的内容只有与 pub
+的关键字。所以,vis
在关心匹配输入的内容是公有还是私有时有用。
此外,如果匹配时,其后没有标记流,整个宏会匹配失败:
++macro_rules! non_optional_vis { + ($vis:vis) => (); +} +non_optional_vis!(); +// ^^^^^^^^^^^^^^^^ error: missing tokens in macro arguments +fn main() {}
重点在于“可能为空”。你可能想到这是隐藏了 ?
+重复操作符的分类符,这样你就不用直接在反复匹配时使用
+?
—— 其实你不能将它和 ?
一起在重复模式匹配中使用。
可以匹配 $vis:vis $ident:ident
,但不能匹配 $(pub)? $ident:ident
,因为 pub
+表明一个有效的标识符,所以后者是模糊不清的。
+macro_rules! vis_ident { + ($vis:vis $ident:ident) => (); +} +vis_ident!(pub foo); // this works fine + +macro_rules! pub_ident { + ($(pub)? $ident:ident) => (); +} +pub_ident!(pub foo); + // ^^^ error: local ambiguity when calling macro `pub_ident`: multiple parsing options: built-in NTs ident ('ident') or 1 other option. +fn main() {}
而且,搭配 tt
分类符和递归展开去匹配空标记也会导致有趣而奇怪的事情。
当 pub
匹配了空标记,元变量依然算一次被捕获,又因为它不是 tt
、ident
或
+lifetime
,所以再次展开时是不清楚的。
这意味着如果这种捕获的结果传递给另一个将它视为 tt
的宏调用,你最终得到一棵空的标记树。
+ +macro_rules! it_is_opaque { + (()) => { "()" }; + (($tt:tt)) => { concat!("$tt is ", stringify!($tt)) }; + ($vis:vis ,) => { it_is_opaque!( ($vis) ); } +} +fn main() { + // this prints "$tt is ", as the recursive calls hits the second branch with + // an empty tt, opposed to matching with the first branch! + println!("{}", it_is_opaque!(,)); +}
++译者注:卫生性 (hygiene) 描述的是 标识符 +在宏处理和展开过程中是“宏定义处的标识符不与外部定义的标识符交互”、“不被外部同名标识符污染的”。见 +卫生性和 Span。
+
Rust 里的声明宏是 部分 卫生的 (partially hygienic 或者称作 mixed hygiene)。
+具体来说,对于以下内容,声明宏是卫生的:
+ +除此之外,声明宏都不是卫生的。1
+推荐尝试 Rust Quiz #24,并阅读 “Truly Hygienic” Let Statements in Rust。
+之所以能做到“卫生”,是因为每个标识符都被赋予了一个看不见的“句法上下文” +(syntax context)。在比较两个标识符时,只有在标识符的原文名称和句法上下文都 +完全一样 的情况下,两个标识符才能被视作等同。
+为阐释这一点,考虑下述代码:
+macro_rules! using_a {
($e:expr) => {
{
let a = 42;
$e
}
}
}
let four = using_a!(a / 10);
+我们将采用背景色来表示句法上下文。现在,将上述宏调用展开如下:
+let four = { let a = 42; a / 10 };+
首先,回想一下,在展开的期间调用声明宏,实际是空(因为那是一棵待补全的语法树)。
+其次,如果我们现在就尝试编译上述代码,编译器将报如下错误:
+error[E0425]: cannot find value `a` in this scope
+ --> src/main.rs:13:21
+ |
+13 | let four = using_a!(a / 10);
+ | ^ not found in this scope
+注意到宏在展开后背景色(即其句法上下文)发生了改变。
+每处宏展开均赋予其内容一个新的、独一无二的上下文。
+故而,在展开后的代码中实际上存在 两个 不同的 a
,它们分别有不同的句法上下文。
+即,第一个 a
与第二个 a
并不相同,即使它们便看起来很像。
也就是说,被替换进宏展开中的标记仍然 保持 着它们原有的句法上下文。
+因为它们是被传给这宏的,并非这宏本身的一部分。因此,我们作出如下修改:
+macro_rules! using_a {
($a:ident, $e:expr) => {
{
let $a = 42;
$e
}
}
}
let four = using_a!(a, a / 10);
+展开如下:
+let four = { let a = 42; a / 10 };+
因为只用了一个 a
(显然 a
在此处是局部变量),编译器将欣然接受此段代码。
$crate
元变量当声明宏需要其定义所在的 (defining) crate
+的其他 items 时,由于“卫生性”,我们需要使用 $crate
元变量。
这个特殊的元变量所做的事情是,它展开成宏所定义的 crate 的绝对路径。
+//// 在 `helper_macro` crate 里定义 `helped!` 和 `helper!` 宏
+#[macro_export]
+macro_rules! helped {
+ // () => { helper!() } // 这行可能导致 `helper` 不在作用域的错误
+ () => { $crate::helper!() }
+}
+
+#[macro_export]
+macro_rules! helper {
+ () => { () }
+}
+
+//// 在另外的 crate 中使用这两个宏
+// 注意:`helper_macro::helper` 并没有导入进来
+use helper_macro::helped;
+
+fn unit() {
+ // 这个宏能运行通过,因为 `$crate` 正确地展开成 `helper_macro` crate 的路径(而不是使用者的路径)
+ helped!();
+}
+请注意,$crate
用在指明非宏的 items 时,它必须和完整且有效的模块路径一起使用。如下:
+ +#![allow(unused)] +fn main() { +pub mod inner { + #[macro_export] + macro_rules! call_foo { + () => { $crate::inner::foo() }; + } + + pub fn foo() {} +} +}
++译者注:这里需要记住的重点是
+ +
有两个标记,当你最终撞见时,很有可能认为它们是标识符 (ident
),但实际上它们不是。
然而正是这些标记,在某些情况下又的确是标识符。
+self
第一个是 self
。毫无疑问,它是一个 关键词 (keyword)。在一般的 Rust
+代码中,不可能出现把它解读成标识符的情况;但在宏中这种情况则有可能发生:
+macro_rules! what_is { + (self) => {"the keyword `self`"}; + ($i:ident) => {concat!("the identifier `", stringify!($i), "`")}; +} + +macro_rules! call_with_ident { + ($c:ident($i:ident)) => {$c!($i)}; +} + +fn main() { + println!("{}", what_is!(self)); + println!("{}", call_with_ident!(what_is(self))); +}
上述代码的输出将是:
+the keyword `self`
+the keyword `self`
+
+但这说不通啊!call_with_ident!
要求一个标识符,而且它的确匹配到了,还成功替换了!所以,
+self
同时是一个关键词,但又不是。你可能会想,好吧,但这鬼东西哪里重要呢?看看这个:
+macro_rules! make_mutable { + ($i:ident) => {let mut $i = $i;}; +} + +struct Dummy(i32); + +impl Dummy { + fn double(self) -> Dummy { + make_mutable!(self); + self.0 *= 2; + self + } +} + +fn main() { + println!("{:?}", Dummy(4).double().0); +}
编译它会失败,并报错:
+error: `mut` must be followed by a named binding
+ --> src/main.rs:2:24
+ |
+2 | ($i:ident) => {let mut $i = $i;};
+ | ^^^^^^ help: remove the `mut` prefix: `self`
+...
+9 | make_mutable!(self);
+ | -------------------- in this macro invocation
+ |
+ = note: `mut` may be followed by `variable` and `variable @ pattern`
+所以说,宏在匹配的时候,会欣然把 self
当作标识符接受,进而允许你把 self
+带到那些实际上没办法使用的情况中去。但是,也成吧,既然得同时记住 self
+既是关键词又是标识符,那下面这个讲道理应该可行,对吧?
+macro_rules! make_self_mutable { + ($i:ident) => {let mut $i = self;}; +} + +struct Dummy(i32); + +impl Dummy { + fn double(self) -> Dummy { + make_self_mutable!(mut_self); + mut_self.0 *= 2; + mut_self + } +} + +fn main() { + println!("{:?}", Dummy(4).double().0); +}
实际上也不行,编译错误变成:
+error[E0424]: expected value, found module `self`
+ --> src/main.rs:2:33
+ |
+2 | ($i:ident) => {let mut $i = self;};
+ | ^^^^ `self` value is a keyword only available in methods with a `self` parameter
+...
+8 | / fn double(self) -> Dummy {
+9 | | make_self_mutable!(mut_self);
+ | | ----------------------------- in this macro invocation
+10 | | mut_self.0 *= 2;
+11 | | mut_self
+12 | | }
+ | |_____- this function has a `self` parameter, but a macro invocation can only access identifiers it receives from parameters
+ |
+这同样也说不通。这简直就像是在抱怨说,它看见的两个 self
不是同一个 self
+... 就搞得像关键词 self
就像标识符一样,也有卫生性。
+macro_rules! double_method { + ($body:expr) => { + fn double(mut self) -> Dummy { + $body + } + }; +} + +struct Dummy(i32); + +impl Dummy { + double_method! {{ + self.0 *= 2; + self + }} +} + +fn main() { + println!("{:?}", Dummy(4).double().0); +}
还是报同样的错。那这个如何:
++macro_rules! double_method { + ($self_:ident, $body:expr) => { + fn double(mut $self_) -> Dummy { + $body + } + }; +} + +struct Dummy(i32); + +impl Dummy { + double_method! {self, { + self.0 *= 2; + self + }} +} + +fn main() { + println!("{:?}", Dummy(4).double().0); +}
终于管用了。所以说,self
是关键词,但如果想它变成标识符,那么同时也能是一个标识符。
那么,相同的道理对类似的其它东西有用吗?
+_
+macro_rules! double_method { + ($self_:ident, $body:expr) => { + fn double($self_) -> Dummy { + $body + } + }; +} + +struct Dummy(i32); + +impl Dummy { + double_method! {_, 0} +} + +fn main() { + println!("{:?}", Dummy(4).double().0); +}
error: no rules expected the token `_`
+ --> src/main.rs:12:21
+ |
+1 | macro_rules! double_method {
+ | -------------------------- when calling this macro
+...
+12 | double_method! {_, 0}
+ | ^ no rules expected this token in macro call
+哈,当然不行。即便它 如同 self
一样从定义上讲是标识符,但 _
+在模式以及表达式中是一个合法的 (valid) 关键词,而不是一个标识符。
你可能觉得,既然 _
在模式中有效,那换成 $self_:pat
是不是就能一石二鸟了呢?
可惜了,也不行,因为 self
不是一个有效的模式。
如果你真想同时匹配这两个标记,仅有的办法是换用 tt
来匹配。
在 Rust 的 2015 和 2018 版本中,导入 macro_rules!
宏是不一样的。
+仍然建议阅读这两部分,因为 2018 版使用的结构在 2015 版中做出了解释。
#[macro_use]
作用域 一章中介绍的 #[macro_use]
属性
+适用于模块或者 external crates
。例如:
+#[macro_use] +mod macros { + macro_rules! X { () => { Y!(); } } + macro_rules! Y { () => {} } +} + +X!(); + +fn main() {}
#[macro_export]
可通过 #[macro_export]
将宏从当前crate
导出。注意,这种方式 无视 所有可见性设定。
定义 lib 包 macs
如下:
mod macros {
+ #[macro_export] macro_rules! X { () => { Y!(); } }
+ #[macro_export] macro_rules! Y { () => {} }
+}
+
+// X! 和 Y! 并非在此处定义的,但它们 **的确** 被导出了(在此处可用)
+// 即便 `macros` 模块是私有的
+下面(在使用 macs
lib 的 crate 中)的代码会正常工作:
X!(); // X 在当前 crate 中被定义
+#[macro_use] extern crate macs; // 从 `macs` 中导入 X
+X!(); // 这里的 X 是最新声明的 X,即 `macs` crate 中导入的 X
+
+fn main() {}
+正如 作用域 一章所说,#[macro_use]
作用于 extern crate
时,
+会强制把导出的宏提到 crate 的顶层模块(根模块),所以这里无须使用 macs::macros
路径。
++注意:只有在根模组中,才可将
+#[macro_use]
用于extern crate
。
在从 extern crate
导入宏时,可显式控制导入 哪些 宏。
+从而利用这一特性来限制命名空间污染,或是覆盖某些特定的宏。就像这样:
// 只导入 `X!` 这一个宏
+#[macro_use(X)] extern crate macs;
+
+// X!(); // X! 已被定义,但 Y! 未被定义。X 与 Y 无关系。
+
+macro_rules! Y { () => {} }
+
+X!(); // X 和 Y 都被定义
+
+fn main() {}
+当导出宏时,常常出现的情况是,宏定义需要其引用所在 crate
内的非宏符号。
+由于 crate
可能被重命名等,我们可以使用一个特殊的替换变量 $crate
。
+它总将被扩展为宏定义所在的 crate
的绝对路径(比如 :: macs
)。
如果你的编译器版本小于 1.30(即 2018 版之前),那么这招并不适用于宏。
+也就是说,你没办法采用类似 $crate::Y!
的代码来引用自己 crate
里的定义的宏。
+这表示结合 #[macro_use]
来选择性导入会无法保证某个名称的宏在另一个 crate 导入同名宏时依然可用。
推荐的做法是,在引用非宏名称时,总是采用绝对路径。 +这样可以最大程度上避免冲突,包括跟标准库中名称的冲突。
+2018 版本让使用 macro_rules!
宏更简单。
+因为新版本设法让 Rust 中某些特殊的东西更像正常的 items 。
+这意味着我们能以命名空间的方式正确导入和使用宏!
因此,不必使用 #[macro_use]
来导入 来自 extern crate 导出的宏 到全局命名空间,
+现在我们这样做就好了:
use some_crate::some_macro;
+
+fn main() {
+ some_macro!("hello");
+ // as well as
+ some_crate::some_other_macro!("macro world");
+}
+可惜,这只适用于导入外部 crate 的宏;
+如果你使用在自己 crate 定义的 macro_rules!
宏,
+那么依然需要把 #[macro_use]
添加到宏所定义的模块上来引入模块里面的宏。
+因而 作用域规则 就像之前谈论的那样生效。
++ ++
$crate
前缀(元变量)在 2018 版中可适用于任何东西, +在 1.31 版之后,宏 和类似 item 的东西都能用$crate
导入了。
一旦语法分析器开始消耗标记以匹配某捕获,整个过程便 无法停止或回溯 。 +这意味着,无论输入是什么样的,下面这个宏的第二项规则将永远无法被匹配到:
++macro_rules! dead_rule { + ($e:expr) => { ... }; + ($i:ident +) => { ... }; +} + +fn main() { + dead_rule!(x+); +}
考虑当以 dead_rule!(x+)
形式调用此宏时,将会发生什么。
+解析器将从第一条规则开始试图进行匹配:它试图将输入解析为一个表达式。
+第一个标记 x
作为表达式是有效的,第二个标记——作为二元加符号 +
的节点——在表达式中也是有效的。
由此你可能会以为,由于输入中并不包含二元加号 +
的右侧元素,
+分析器将会放弃尝试这一规则,转而尝试下一条规则。
+实则不然:分析器将会 panic 并终止整个编译过程,最终返回一个语法错误。
由于分析器的这一特点,下面这点尤为重要: +一般而言,在书写宏规则时,应从最具体的开始写起,依次写直到最不具体的 。
+为防止将来的语法变动影响宏输入的解析方式,
+macro_rules!
对紧接元变量后的内容施加了限制。
+在 Rust 1.52 中,能够紧跟片段分类符后面的内容具有如下限制1:
stmt
和 expr
:=>
、,
、;
之一pat
:=>
、,
、=
、if
、in
之一2pat_param
]:=>
、,
、=
、|
、if
、in
之一path
和 ty
:=>
、,
、=
、|
、;
、:
、>
、>>
、[
、{
、as
、where
之一;
+或者 block
型的元变量vis
:,
、除了 priv
之外的标识符、任何以类型开头的标记、
+ident
或 ty
或 path
型的元变量使用 2021 edition 之前的 Rust,pat
依然可以跟随 |
。
反复匹配的情况也遵循这些限制1,也就是说:
+如果一个重复操作符(*
或 +
)能让一类元变量重复数次,
+那么反复出现的内容就是这类元变量,反复结束之后所接的内容遵照上面的限制。
如果一个重复操作符(*
或 ?
)让一类元变量重复零次,
+那么元变量之后的内容遵照上面的限制。
内容来自于 Reference +follow-set-ambiguity-restrictions 一节。
+解析器不会预先运行代码,这意味着如果编译器不能一次就唯一地确定如何解析宏调用, +那么编译器就带着模糊的报错信息而终止运行。 +一个触发终止运行的例子是:
++macro_rules! ambiguity { + ($($i:ident)* $i2:ident) => { }; +} + +// error: +// local ambiguity: multiple parsing options: built-in NTs ident ('i') or ident ('i2'). +fn main() { ambiguity!(an_identifier); }
编译器不会提前看到传入的标识符之后是不是一个 )
,如果提前看到的话就会解析正确。
关于代换元变量 (substitution,这里指把已经进行宏解析的 token 再次传给宏) , +常常让人惊讶的一面是,尽管 很像 是根据标记 (token) 进行代换的,但事实并非如此 +——代换基于已经解析的 AST 节点。
+思考下面的例子:
++macro_rules! capture_then_match_tokens { + ($e:expr) => {match_tokens!($e)}; +} + +macro_rules! match_tokens { + ($a:tt + $b:tt) => {"got an addition"}; + (($i:ident)) => {"got an identifier"}; + ($($other:tt)*) => {"got something else"}; +} + +fn main() { + println!("{}\n{}\n{}\n", + match_tokens!((caravan)), + match_tokens!(3 + 6), + match_tokens!(5)); + println!("{}\n{}\n{}", + capture_then_match_tokens!((caravan)), + capture_then_match_tokens!(3 + 6), + capture_then_match_tokens!(5)); +}
其结果:
+got an identifier
+got an addition
+got something else
+
+got something else
+got something else
+got something else
+
+通过解析已经传入 AST 节点的输入,代换的结果变得 很稳定:你再也无法检查其内容了, +也不再匹配内容。
+另一个例子可能也会很令人困惑:
++macro_rules! capture_then_what_is { + (#[$m:meta]) => {what_is!(#[$m])}; +} + +macro_rules! what_is { + (#[no_mangle]) => {"no_mangle attribute"}; + (#[inline]) => {"inline attribute"}; + ($($tts:tt)*) => {concat!("something else (", stringify!($($tts)*), ")")}; +} + +fn main() { + println!( + "{}\n{}\n{}\n{}", + what_is!(#[no_mangle]), + what_is!(#[inline]), + capture_then_what_is!(#[no_mangle]), + capture_then_what_is!(#[inline]), + ); +}
结果是:
+no_mangle attribute
+inline attribute
+something else (#[no_mangle])
+something else (#[inline])
+
+避免这个意外情况的唯一方式就是使用 tt
、ident
或者 lifetime
分类符。
+每当你捕获到除此之外的分类符,结果将只能被用于直接输出。
+比如这里使用的 stringify!
3,它是一条内置于编译器的语法拓展
+(查看源码可知),
+将所有输入标记结合在一起,作为单个字符串输出。
这里未包含原作对 stringify!
用于替换 (substitution) 场景的 解读,因为那个例子的结果有些变化。
++RFC: rfcs#1584
+
+Tracking Issue: rust#83527
+Feature:#![feature(macro_metavar_expr)]
++注意:示例代码片段非常简单,只试图展示它们是如何工作的。
+
+关于这些元变量表达式,如果你认为你有合适的、单独使用的小片段,请提交它们!
正如在 思路 中提到的,Rust 有一些特殊的元变量表达式(以下简称表达式):transcriber1 +可以使用这些表达式来获取有关元变量的信息。如果没有这些表达式,它们所提供的信息就很难甚至不可能获得。
+本章将结合用例对它们进行更深入的介绍。
+ +译者注:在专业的讨论中,尤其涉及元变量表达式,常用 transcribe(r) 一词而不使用 expand (expansion)。
+$$
)$$
表达式展开为单个 $
,实际上使其成为转义的 $
。这让声明宏宏生成新的声明宏。
因为以前的声明宏将无法转义 $
,所以无法使用元变量、重复和元变量表达式。例如以下代码片段中不使用 $$
,就无法使用 bar!
:
+macro_rules! foo { + () => { + macro_rules! bar { + ( $( $any:tt )* ) => { $( $any )* }; + // ^^^^^^^^^^^ error: attempted to repeat an expression containing no syntax variables matched as repeating at this depth + } + }; +} + +foo!(); +fn main() {}
问题很明显, foo!
的 transcriber 看到有反复捕获的意图,并试图反复捕获,但它的作用域中没有 $any
+元变量,这导致它产生错误。有了 $$
,我们就可以解决这个问题2,因为 foo
的 transcriber 不再尝试反复捕获。
+#![feature(macro_metavar_expr)] + +macro_rules! foo { + () => { + macro_rules! bar { + ( $$( $$any:tt )* ) => { $$( $$any )* }; + } + }; +} + +foo!(); +bar!(); +fn main() {}
译者注:在没有 $$
之前,存在一种技巧绕过这里的问题:你可以使用 $tt
捕获 $
来进行转义,比如这样。
count(ident, depth)
count
表达式展开成元变量 $ident
在给定反复深度的反复次数。
ident
参数必须是规则作用域中声明的元变量depth
参数必须是值小于或等于元变量 $ident
出现的最大反复深度的整型字面值count(ident, depth)
展开成不带后缀的整型字面值标记count(ident)
是 count(ident, 0)
的简写+#![feature(macro_metavar_expr)] + +macro_rules! foo { + ( $( $outer:ident ( $( $inner:ident ),* ) ; )* ) => { + println!("count(outer, 0): $outer repeats {} times", ${count($outer)}); + println!("count(inner, 0): The $inner repetition repeats {} times in the outer repetition", ${count($inner, 0)}); + println!("count(inner, 1): $inner repeats {} times in the inner repetitions", ${count($inner, 1)}); + }; +} + +fn main() { + foo! { + outer () ; + outer ( inner , inner ) ; + outer () ; + outer ( inner ) ; + }; +}
index(depth)
index(depth)
表达式展开为给定反复深度下,当前的迭代索引。
depth
参数表明在第几层反复,这个数字从最内层反复调用表达式开始向外计算index(depth)
展开成不带后缀的整型字面值标记index()
是 index(0)
的简写+#![feature(macro_metavar_expr)] + +macro_rules! attach_iteration_counts { + ( $( ( $( $inner:ident ),* ) ; )* ) => { + ( $( + $(( + stringify!($inner), + ${index(1)}, // 这指的是外层反复 + ${index()} // 这指的是内层反复,等价于 `index(0)` + ),)* + )* ) + }; +} + +fn main() { + let v = attach_iteration_counts! { + ( hello ) ; + ( indices , of ) ; + () ; + ( these, repetitions ) ; + }; + println!("{v:?}"); +}
length(depth)
length(depth)
表达式展开为在给定反复深度的迭代次数。
depth
参数表示在第几层反复,这个数字从最内层反复调用表达式开始向外计算length(depth)
展开成不带后缀的整型字面值标记length()
是 length(0)
的简写+#![feature(macro_metavar_expr)] + +macro_rules! lets_count { + ( $( $outer:ident ( $( $inner:ident ),* ) ; )* ) => { + $( + $( + println!( + "'{}' in inner iteration {}/{} with '{}' in outer iteration {}/{} ", + stringify!($inner), ${index()}, ${len()}, + stringify!($outer), ${index(1)}, ${len(1)}, + ); + )* + )* + }; +} + +fn main() { + lets_count!( + many (small , things) ; + none () ; + exactly ( one ) ; + ); +}
ignore(ident)
ignore(ident)
表达式展开为空,这使得在无需实际展开元变量的时候,像元变量反复展开相同次数的某些内容。
ident
参数必须是规则作用域中声明的元变量+ +#![feature(macro_metavar_expr)] + +macro_rules! repetition_tuples { + ( $( ( $( $inner:ident ),* ) ; )* ) => { + ($( + $( + ( + ${index()}, + ${index(1)} + ${ignore($inner)} // without this metavariable expression, compilation would fail + ), + )* + )*) + }; +} + +fn main() { + let tuple = repetition_tuples!( + ( one, two ) ; + () ; + ( one ) ; + ( one, two, three ) ; + ); + println!("{tuple:?}"); +}
++这部分最新的内容可参考 Reference 的 +scoping-exporting-and-importing +一节。部分翻译内容引自 Reference +中文版。
+
函数式宏的作用域规则可能有一点反直觉。(函数式宏包括声明宏与函数式过程宏。) +由于历史原因,宏的作用域并不完全像各种程序项那样工作。
+有两种形式的作用域:文本作用域 (textual scope) 和 基于路径的作用域 (path-based scope)。
+文本作用域:基于宏在源文件中(定义和使用所)出现的顺序,或是跨多个源文件出现的顺序, +文本作用域是默认的作用域。
+基于路径的作用域:与其他程序项作用域的运行方式相同。
+当声明宏被 非限定标识符(unqualified identifier,非多段路径段组成的限定性路径)调用时, +会首先在文本作用域中查找。 +如果文本作用域中没有任何结果,则继续在基于路径的作用域中查找。
+如果宏的名称由路径限定 (qualified with a path) ,则只在基于路径的作用域中查找。
+与 Rust 语言其余所有部分都不同的是,函数式宏在子模块中仍然可见。
++macro_rules! X { () => {}; } +mod a { + X!(); // defined +} +mod b { + X!(); // defined +} +mod c { + X!(); // defined +} +fn main() {}
++注意:即使子模组的内容处在不同文件中,这些例子中所述的行为仍然保持不变。
+
同样与 Rust 语言其余所有部分不同,宏只有在其定义 之后 可见。 +下例展示了这一点。同时注意到,它也展示了宏不会“漏出” (leak) 其定义所在的作用域:
++mod a { + // X!(); // undefined +} +mod b { + // X!(); // undefined + macro_rules! X { () => {}; } + X!(); // defined +} +mod c { + // X!(); // undefined +} +fn main() {}
要清楚,即使你把宏移动到外层作用域,词法依赖顺序的规则依然适用。
++mod a { + // X!(); // undefined +} + +macro_rules! X { () => {}; } + +mod b { + X!(); // defined +} +mod c { + X!(); // defined +} +fn main() {}
然而对于宏自身来说,这种具有顺序的依赖行为不存在。 +即被调用的宏可以先于调用宏之前声明:
++mod a { + // X!(); // undefined +} + +macro_rules! X { () => { Y!(); }; } // 注意这里的代码运行通过 + +mod b { + // 注意这里 X 虽然被定义,但是 Y 不被定义,所以不能使用 X + // X!(); // defined, but Y! is undefined +} + +macro_rules! Y { () => {}; } + +mod c { + X!(); // defined, and so is Y! +} +fn main() {}
允许多次定义 macro_rules!
宏,最后声明的宏会简单地覆盖 (shadow) 上一个声明的同名宏;
+如果最后声明的宏离开作用域,上一个宏在有效的作用域内还能被使用。
+macro_rules! X { (1) => {}; } +X!(1); +macro_rules! X { (2) => {}; } +// X!(1); // Error: no rule matches `1` +X!(2); + +mod a { + macro_rules! X { (3) => {}; } + // X!(2); // Error: no rule matches `2` + X!(3); +} +// X!(3); // Error: no rule matches `3` +X!(2); + +fn main() { }
#[macro_use]
属性这个属性放置在宏定义所在的模块前 或者 extern crate
语句前。
#[macro_use]
属性:导出该模块内的所有宏,
+从而让导出的宏在所定义的模块结束之后依然可用。+mod a { + // X!(); // undefined +} + +#[macro_use] +mod b { + macro_rules! X { () => {}; } + X!(); // defined +} + +mod c { + X!(); // defined +} +fn main() {}
注意,这可能会产生一些奇怪的后果,因为宏(包括过程宏)中的标识符只有在宏展开的过程中才会被解析。
++mod a { + // X!(); // undefined +} + +#[macro_use] +mod b { + macro_rules! X { () => { Y!(); }; } + // X!(); // defined, but Y! is undefined +} + +macro_rules! Y { () => {}; } + +mod c { + X!(); // defined, and so is Y! +} +fn main() {}
extern crate
语句加上 #[macro_use]
属性:
+把外部 crate 定义且导出的宏引入当前 crate 的根/顶层模块。(当前 crate 使用外部 crate)假设在外部名称为 mac
的 crate 中定义了 X!
宏,在当前模块:
//// 这里的 `X!` 与 `Y!` 无关,前者定义于外部 crate,后者定义于当前 crate
+
+mod a {
+ // X!(); // defined, but Y! is undefined
+}
+
+macro_rules! Y { () => {}; }
+
+mod b {
+ X!(); // defined, and so is Y!
+}
+
+#[macro_use] extern crate macs;
+mod c {
+ X!(); // defined, and so is Y!
+}
+
+fn main() {}
+前四条作用域规则同样适用于函数。
+至于第五条规则, #[macro_use]
属性并不直接作用于函数。
+macro_rules! X { + () => { Y!() }; +} + +fn a() { + macro_rules! Y { () => {"Hi!"} } + assert_eq!(X!(), "Hi!"); + { + assert_eq!(X!(), "Hi!"); + macro_rules! Y { () => {"Bye!"} } + assert_eq!(X!(), "Bye!"); + } + assert_eq!(X!(), "Hi!"); +} + +fn b() { + macro_rules! Y { () => {"One more"} } + assert_eq!(X!(), "One more"); +} + +fn main() { + a(); + b(); +}
由于前述种种规则,一般来说,
+建议将所有应对整个 crate
均可见的宏的定义置于根模块的最顶部,
+借以确保它们 一直 可用。
+这个建议和适用于在文件 mod
定义的宏:
#[macro_use]
+mod some_mod_that_defines_macros;
+mod some_mod_that_uses_those_macros;
+这里的顺序很重要,因为第二个模块依赖于第一个模块的宏, +所以改变这两个模块的顺序会无法编译。
+Rust 的 macro_rules!
宏 默认并没有基于路径的作用域。
然而,如果这个宏被加上 #[macro_export]
属性,那么它就在 crate 的根作用域里被定义,
+而且能直接使用它。
导入/导出宏 一章会更深入地探讨这个属性。
+ +解析和展开模式。
+ ++macro_rules! call_with_larch { + ($callback:ident) => { $callback!(larch) }; +} + +macro_rules! expand_to_larch { + () => { larch }; +} + +macro_rules! recognize_tree { + (larch) => { println!("#1, the Larch.") }; + (redwood) => { println!("#2, the Mighty Redwood.") }; + (fir) => { println!("#3, the Fir.") }; + (chestnut) => { println!("#4, the Horse Chestnut.") }; + (pine) => { println!("#5, the Scots Pine.") }; + ($($other:tt)*) => { println!("I don't know; some kind of birch maybe?") }; +} + +fn main() { + recognize_tree!(expand_to_larch!()); // 无法直接使用 `expand_to_larch!` 的展开结果 + call_with_larch!(recognize_tree); // 回调就是给另一个宏传入宏的名称 (`ident`),而不是宏的结果 +} + +// 打印结果: +// I don't know; some kind of birch maybe? +// #1, the Larch.
由于宏展开的机制限制,(至少在最新的 Rust 中) +不可能做到把一例宏的展开结果作为有效信息提供给另一例宏。 +这为宏的模块化工作施加了难度。
+使用递归并传递回调 (callbacks) 是条出路。 +作为演示,上例两处宏调用的展开过程如下:
+recognize_tree! { expand_to_larch ! ( ) }
+println! { "I don't know; some kind of birch maybe?" }
+// ...
+
+call_with_larch! { recognize_tree }
+recognize_tree! { larch }
+println! { "#1, the Larch." }
+// ...
+可以反复匹配 tt
来将任意参数转发给回调:
+macro_rules! callback { + ($callback:ident( $($args:tt)* )) => { + $callback!( $($args)* ) + }; +} + +fn main() { + callback!(callback(println("Yes, this *was* unnecessary."))); +}
如果需要的话,当然还可以在参数中增加额外的标记 (tokens) 。
+ ++#[macro_export] +macro_rules! foo { + (@as_expr $e:expr) => {$e}; + + ($($tts:tt)*) => { + foo!(@as_expr $($tts)*) + }; +} + +fn main() { + assert_eq!(foo!(42), 42); +}
内用规则可用在以下两种情况:
+TT
“撕咬机” 的读写。那么为什么将多个宏统一为一个有用呢? +主要原因是:在 2015 版本中,未对宏进行空间命名。这导致一个问题——必须重新导出内部定义的所有宏, +从而污染整个全局宏命名空间;更糟糕的是,宏与其他 crate 的同名宏发生冲突。 +简而言之,这很造成很多麻烦。 +幸运的是,在 rustc版本 >= 1.30 的情况下(即 2018 版本之后), +这不再是问题了(但是内用规则可以减少不必要声明的宏), +有关宏导出更多信息,请参阅本书 导入/导出宏 。
+好了,让我们讨论如何利用“内用规则” (internal rules) 来把多个宏统一为一个, +以及“内用规则”这项技术到底是什么吧。
+这个例子有两个宏,一个常见的 as_expr!
宏
+和 foo!
宏,后者使用了前者。如果分开写就是下面的形式:
+#[macro_export] +macro_rules! as_expr { ($e:expr) => {$e} } + +#[macro_export] +macro_rules! foo { + ($($tts:tt)*) => { + as_expr!($($tts)*) + }; +} + +fn main() { + assert_eq!(foo!(42), 42); +}
这当然不是最好的解决办法,正如前面提到的,因为 as_expr
污染了全局宏命名空间。
+在这个特定的例子里,as_expr
只是一个简单的宏,它只会被使用一次,
+所以,利用内用规则,把它“嵌入”到 foo
这个宏里面吧!
在 foo
仅有的一条规则前面添加一条新匹配模式(新规则),
+这个匹配模式由 as_expr
组成(和命名),然后附加上宏的输入参数 $e:expr
;
+在展开里填写这个宏被匹配到时具体的内容。从而得到本章开头的代码:
+#[macro_export] +macro_rules! foo { + (@as_expr $e:expr) => {$e}; + + ($($tts:tt)*) => { + foo!(@as_expr $($tts)*) + }; +} + +fn main() { + assert_eq!(foo!(42), 42); +}
可以看到,没有调用 as_expr
宏,而是递归调用在参数前放置了特殊标记树的 foo!(@as_expr $($tts)*)
。
+要是你看得仔细些,你甚至会发现这个模式能好地结合 TT
撕咬机 !
之所以用 @
,是因为在 Rust 1.2 下,该标记尚无任何在前缀位置的用法;
+因此,这个语法定义在当时不会与任何东西撞车。
+如果你想用别的符号或特有前缀都可以(比如试试 #
、!
),
+但 @
的用例已被传播开来,因此,使用它可能更容易帮助读者理解你的代码。
++注意:
+@
符号很早之前曾作为前缀被用于表示被垃圾回收了的指针, +那时 Rust 还在采用各种记号代表指针类型。而现在的
+@
只有一种用法: +将名称绑定至模式中(譬如match
的模式匹配中)。 +在这种用法中它是中缀运算符,与我们的上述用例并不冲突。
还有一点要注意,内用规则通常应排在“真正的”规则之前。
+这样做可避免 macro_rules!
错把内用规则调用解析成别的东西,比如表达式。
内用规则的一个缺点是它们会增加编译时间。
+即便最终只有一条规则的宏可以匹配(有效的)宏调用,但编译器必须尝试按顺序匹配所有规则。
+如果宏有许多规则,则可能会有许多匹配失败的情况,而使用内部规则会增加此类匹配失败的数量。
+此外,@as_expr
方式的标识符使规则变得更长,这略微增加了编译器在匹配时必须做的工作量。
因此,为了获得最佳性能,最好避免使用内部规则。
+避免使用它们通常也会使复杂的宏更易于阅读。
+ ++#![allow(unused)] +fn main() { +macro_rules! init_array { + (@accum (0, $_e:expr) -> ($($body:tt)*)) + => {init_array!(@as_expr [$($body)*])}; + (@accum (1, $e:expr) -> ($($body:tt)*)) + => {init_array!(@accum (0, $e) -> ($($body)* $e,))}; + (@accum (2, $e:expr) -> ($($body:tt)*)) + => {init_array!(@accum (1, $e) -> ($($body)* $e,))}; + (@accum (3, $e:expr) -> ($($body:tt)*)) + => {init_array!(@accum (2, $e) -> ($($body)* $e,))}; + (@as_expr $e:expr) => {$e}; + [$e:expr; $n:tt] => { + { + let e = $e; + init_array!(@accum ($n, e.clone()) -> ()) + } + }; +} + +let strings: [String; 3] = init_array![String::from("hi!"); 3]; +assert_eq!(format!("{:?}", strings), "[\"hi!\", \"hi!\", \"hi!\"]"); +}
在 Rust 中,所有宏最终 必须 展开为一个完整、有效的句法元素(比如表达式、条目等等)。 +这意味着,不可能定义一个最终展开为残缺构造的宏。
+有些人可能希望,上例中的宏能被更加直截了当地表述成:
+macro_rules! init_array {
+ (@accum 0, $_e:expr) => {/* empty */};
+ (@accum 1, $e:expr) => {$e};
+ (@accum 2, $e:expr) => {$e, init_array!(@accum 1, $e)};
+ (@accum 3, $e:expr) => {$e, init_array!(@accum 2, $e)};
+ [$e:expr; $n:tt] => {
+ {
+ let e = $e;
+ [init_array!(@accum $n, e)]
+ }
+ };
+}
+
+他们预期的展开过程如下:
+ [init_array!(@accum 3, e)]
+ [e, init_array!(@accum 2, e)]
+ [e, e, init_array!(@accum 1, e)]
+ [e, e, e]
+然而,这一思路中,每个中间步骤的展开结果都是一个不完整的表达式。 +即便这些中间结果对外部来说绝不可见,Rust 仍然禁止这种用法。
+下推累积 (push-down accumulation) 则使我们得以在完全完成之前毋需考虑构造的完整性, +进而累积构建出我们所需的标记序列。 +本章开头给出的示例中,宏调用的展开过程如下:
+init_array! { String:: from ( "hi!" ) ; 3 }
+init_array! { @ accum ( 3 , e . clone ( ) ) -> ( ) }
+init_array! { @ accum ( 2 , e.clone() ) -> ( e.clone() , ) }
+init_array! { @ accum ( 1 , e.clone() ) -> ( e.clone() , e.clone() , ) }
+init_array! { @ accum ( 0 , e.clone() ) -> ( e.clone() , e.clone() , e.clone() , ) }
+init_array! { @ as_expr [ e.clone() , e.clone() , e.clone() , ] }
+可以修改一下代码,看到每次调用时 $($body)*
存储的内容变化:
+macro_rules! init_array { + (@accum (0, $_e:expr) -> ($($body:tt)*)) + => {init_array!(@as_expr [$($body)*])}; + (@accum (1, $e:expr) -> ($($body:tt)*)) + => {init_array!(@accum (0, $e) -> ($($body)* $e+3,))}; + (@accum (2, $e:expr) -> ($($body:tt)*)) + => {init_array!(@accum (1, $e) -> ($($body)* $e+2,))}; + (@accum (3, $e:expr) -> ($($body:tt)*)) + => {init_array!(@accum (2, $e) -> ($($body)* $e+1,))}; + (@as_expr $e:expr) => {$e}; + [$e:expr; $n:tt $(; first $init:expr)?] => { + { + let e = $e; + init_array!(@accum ($n, e.clone()) -> ($($init)?,)) + } + }; +} + +fn main() { + let array: [usize; 4] = init_array![0; 3; first 0]; + println!("{:?}", array); +}
根据 调试 一章的内容,
+在 nightly Rust 中使用编译命令:
+cargo rustc --bin my-project -- -Z trace-macros
,即得到以下输出:
note: trace_macro
+ --> src/main.rs:20:31
+ |
+20 | let array: [usize; 4] = init_array![0; 3; first 0];
+ | ^^^^^^^^^^^^^^^^^^^^^^^^^^
+ |
+ = note: expanding `init_array! { 0 ; 3 ; first 0 }`
+ = note: to `{ let e = 0 ; init_array! (@ accum(3, e.clone()) -> (0,)) }`
+ = note: expanding `init_array! { @ accum(3, e.clone()) -> (0,) }`
+ = note: to `init_array! (@ accum(2, e.clone()) -> (0, e.clone() + 1,))`
+ = note: expanding `init_array! { @ accum(2, e.clone()) -> (0, e.clone() + 1,) }`
+ = note: to `init_array! (@ accum(1, e.clone()) -> (0, e.clone() + 1, e.clone() + 2,))`
+ = note: expanding `init_array! { @ accum(1, e.clone()) -> (0, e.clone() + 1, e.clone() + 2,) }`
+ = note: to `init_array!
+ (@ accum(0, e.clone()) -> (0, e.clone() + 1, e.clone() + 2, e.clone() + 3,))`
+ = note: expanding `init_array! { @ accum(0, e.clone()) -> (0, e.clone() + 1, e.clone() + 2, e.clone() + 3,) }`
+ = note: to `init_array! (@ as_expr [0, e.clone() + 1, e.clone() + 2, e.clone() + 3,])`
+ = note: expanding `init_array! { @ as_expr [0, e.clone() + 1, e.clone() + 2, e.clone() + 3,] }`
+ = note: to `[0, e.clone() + 1, e.clone() + 2, e.clone() + 3]`
+可以看到,每一步都在累积输出,直到规则完成,给出完整的表达式。
+上述过程的关键点在于,使用 $($body:tt)*
来保存输出中间值,
+而不触发其它解析机制。采用 ($input) -> ($output)
+的形式仅是出于传统,用以明示此类宏的作用。
由于可以存储任意复杂的中间结果,
+下推累积在构建 TT
撕咬机 的过程中经常被用到。
+当构造类似于这个例子的宏时,也会结合 内用规则。
下推累积本质上是二次复杂度的。考虑一个包含 100 +个标记树的累加器1,每次调用一个标记树:
+这是一个典型的二次复杂度模式,长输入会导致宏延长编译时间。
+此外,TT 撕咬机对其输入也是天生的二次复杂度,所以同时使用 +TT 撕咬机和下推累积的宏将是双倍二次的!
+所有关于 TT 撕咬机的性能建议都适用于下推积累。
+一般来说,避免过多地使用它们,并尽可能地让它们的简单。
+最后,确保将累加器放在规则的末尾,而不是开头。
+这样,如果匹配规则失败,编译器就不必匹配(可能很长的)累加器,从而避免遇到规则中不匹配的部分。这可能会对编译时间产生很大影响。
+译者注:accumulator,即使用下推累积方式编写的声明宏。
+macro_rules! replace_expr {
+ ($_t:tt $sub:expr) => {$sub};
+}
+在上面代码的模式中,匹配到的重复序列将被直接丢弃, +仅留用它所带来的长度信息(以及元素的类型信息); +且原本标记所在的位置将被替换成某种重复元素。
+举个例子,考虑如何为一个元素多于12个 (Rust 1.2 下的元组元素个数的最大值)
+的 tuple
提供默认值。
+macro_rules! tuple_default { + ($($tup_tys:ty),*) => { + ( + $( + replace_expr!( + ($tup_tys) + Default::default() + ), + )* + ) + }; +} + +macro_rules! replace_expr { + ($_t:tt $sub:expr) => { + $sub + }; +} + +fn main() { + assert_eq!(tuple_default!(i32, bool, String), + (i32::default(), bool::default(), String::default())); +}
++仅对此例: +我们其实可以直接用
+$tup_tys::default()
。
上例中,我们 并未真正使用 匹配到的类型。 +实际上,我们把它丢弃了,并用一个表达式重复替代 (repetition replacement) 。 +换句话说,我们实际关心的不是有哪些类型,而是有多少个类型。
+ +TT
捆绑+macro_rules! call_a_or_b_on_tail { + ((a: $a:ident, b: $b:ident), call a: $($tail:tt)*) => { + $a(stringify!($($tail)*)) + }; + + ((a: $a:ident, b: $b:ident), call b: $($tail:tt)*) => { + $b(stringify!($($tail)*)) + }; + + ($ab:tt, $_skip:tt $($tail:tt)*) => { + call_a_or_b_on_tail!($ab, $($tail)*) + }; +} + +fn compute_len(s: &str) -> Option<usize> { + Some(s.len()) +} + +fn show_tail(s: &str) -> Option<usize> { + println!("tail: {:?}", s); + None +} + +fn main() { + assert_eq!( + call_a_or_b_on_tail!( + (a: compute_len, b: show_tail), + the recursive part that skips over all these + tokens doesn't much care whether we will call a + or call b: only the terminal rules care. + ), + None + ); + assert_eq!( + call_a_or_b_on_tail!( + (a: compute_len, b: show_tail), + and now, to justify the existence of two paths + we will also call a: its input should somehow + be self-referential, so let's make it return + some eighty-six! + ), + Some(92) + ); +}
在十分复杂的递归宏中,可能需要非常多的参数, +才足以在每层调用之间传递必要的标识符与表达式。 +然而,根据实现上的差异,可能存在许多这样的中间层, +它们转发了 (forward) 这些参数,但并没有用到。
+因此,将所有这些参数捆绑 (bundle) 在一起,通过分组将其放进单独一棵标记树 tt
里,
+可以省事许多。这样一来,那些用不到这些参数的递归层可以直接捕获并替换这棵标记树,
+而不需要把整组参数完完全全准准确确地捕获替换掉。
上面的例子把表达式 $a
和 $b
捆绑起来,
+然后作为一棵 tt
交由递归规则处理。
+随后,终结规则 (terminal rules) 将这组标记解构 (destructure) ,
+并访问其中的表达式。
TT
“撕咬机”++译者注:原文标题为 incremental
+TT
muncher 。
+macro_rules! mixed_rules { + () => {}; + (trace $name:ident; $($tail:tt)*) => { + { + println!(concat!(stringify!($name), " = {:?}"), $name); + mixed_rules!($($tail)*); + } + }; + (trace $name:ident = $init:expr; $($tail:tt)*) => { + { + let $name = $init; + println!(concat!(stringify!($name), " = {:?}"), $name); + mixed_rules!($($tail)*); + } + }; +} + +fn main() { + let a = 42; + let b = "Ho-dee-oh-di-oh-di-oh!"; + let c = (false, 2, 'c'); + mixed_rules!( + trace a; + trace b; + trace c; + trace b = "They took her where they put the crazies."; + trace b; + ); +}
此模式可能是 最强大 的宏解析技巧。通过使用它,一些极其复杂的语法都能得到解析。
+“标记树撕咬机” (TT
muncher) 是一种递归宏,其工作机制有赖于对输入的顺次、逐步处理
+(incrementally processing) 。处理过程的每一步中,它都将匹配并移除(“撕咬”掉)输入头部
+(start) 的一列标记 (tokens),得到一些中间结果,然后再递归地处理输入剩下的尾部。
名称中含有“标记树”,是因为输入中尚未被处理的部分总是被捕获在 $($tail:tt)*
+的形式中。之所以如此,是因为只有通过使用反复匹配 tt
才能做到 无损地
+(losslessly) 捕获住提供给宏的输入部分。
标记树撕咬机仅有的限制,也是整个宏系统的局限:
+macro_rules!
捕获到的字面值和语法结构。然而,需要把宏递归的局限性纳入考量。macro_rules!
没有做任何形式的尾递归消除或优化。
在写标记树撕咬机时,建议多花些功夫,尽可能地限制递归调用的次数。
+以下两种做法帮助你做到限制宏递归:
+例子见 计数-递归
+++译者注:要点是
++
+- 可以一次处理很多标记来减少递归次数(比如运用反复匹配)
+- 可以编写规则简单的宏,然后多次调用
+- 把容易匹配到的规则放到前面,以减少匹配次数(因为规则顺序决定了匹配顺序)
+
TT 撕咬机天生就是二次复杂度的。考虑一个 TT 撕咬机 +规则,它消耗一个标记树,然后递归地在其余输入上调用自身。如果向其传递 100 个标记树:
+这是一个典型的二次复杂度模式,过长的输入会导致宏展开延长编译时间。
+因此,尽量避免过多地使用 TT 撕咬机,特别是在输入较长的情况下。
+recursion_limit
属性的缺省值 (目前是 128 )
+是一个良好的健全性检查;如果你必须超过它,那么可能会遇到麻烦。
建议是,你可以选择编写一个:
+例如,别这样写:
++#![allow(unused)] +fn main() { +macro_rules! f { ($($tt:tt)*) => {} } +f! { + fn f_u8(x: u32) -> u8; + fn f_u16(x: u32) -> u16; + fn f_u32(x: u32) -> u32; + fn f_u64(x: u64) -> u64; + fn f_u128(x: u128) -> u128; +} +}
应该这样写:
++#![allow(unused)] +fn main() { +macro_rules! f { ($($tt:tt)*) => {} } +f! { fn f_u8(x: u32) -> u8; } +f! { fn f_u16(x: u32) -> u16; } +f! { fn f_u32(x: u32) -> u32; } +f! { fn f_u64(x: u64) -> u64; } +f! { fn f_u128(x: u128) -> u128; } +}
宏的输入越长,第二种编写方式就越有可能缩短编译时间。
+此外,如果 TT 撕咬机有许多规则,请 尽可能把最频繁匹配的规则放到前面 +。这避免了不必要的匹配失败。(事实上,这对任何类型的声明性宏都是很好的建议,而不仅仅是 +TT 撕咬机。)
+最后,优先使用正常的反复匹配(*
或 +
)来编写宏,这比 TT 撕咬机更好。如果每次调用
+TT 撕咬机时,一次只处理一个标记,则最有可能出现这种情况。
在更复杂的情况下,可以参考 quote!
使用的一种高级技术,它可以避免二次复杂度,而且不会达到递归上限,但代价是一些复杂的概念。详情请参考此处。
++注意:这是对 Daniel Keep 撰写的书 +的续写,自 2016 年初夏以来,那本书就没再更新。
+
本书的续写者为 Veykril,使用 +mdBook 工具生成。你可以浏览本书的 +英文版本,和 github +仓库。1
+这本书尝试提炼出 Rust 社区对 Rust 宏的共识,准确地说,是 通过例子 来讲述宏2。 +因此,欢迎 PR 补充和提 issue。
+如果你对某些书中的内容不清楚,或者不理解,别害怕提 issue +来请求澄清那部分。本书的目标是尽可能成为最好的(宏)学习资料。
+在我学习 Rust 的时候,Little Book of Rust Macros 原作 +通过例子 的方式非常给力地帮助过我理解(声明)宏。很遗憾的是,Rust +语言与宏系统持续改进时,原作者不再更新书籍。
+这也是我想尽可能地更新这本书的原因,并且我尽可能地把新发现的事情增加到书中,以帮助新的 +Rust 宏学习者理解宏系统 —— 这个让很多人困惑的部分。
+++这本书认为你应该对 Rust 有基本的了解,它不会解释 Rust +语言特性或者与宏无关的结构,但不会假设你提前掌握宏的知识。
+你必须至少阅读和理解了 Rust Book +的前七章 —— 当然,建议你阅读完 Rust Book 的大部分内容。
+
译者注:我对原作和续作进行了梳理,见 翻译说明。
+译者注:2022 年的中文版随续作更新了过程宏,而声明宏也一直在演进。
+非常感谢 Daniel Keep 最初写下这本书。3
+感谢对原书提出建议和更正的读者: +IcyFoxy、 Rym、 TheMicroWorm、 Yurume、 akavel、 cmr、 eddyb、 ogham 和 snake_case。
+译者注:非常感谢 Veykril 不懈地更新此书。感谢 +DaseinPhaos 对原作的翻译。此外,本书的右侧 +TOC 是由 mdbook-theme 所提供。
+这本书沿袭了原作的版权声明,因此具有 CC BY-SA 4.0 和 MIT license 的双重许可。
+ +