卫生性
译者注:卫生性 (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() {} } }