实例宏
语法
宏规则定义 :
macro_rules
!
标识符 宏规则组定界宏规则组定界 :
(
宏规则组)
;
|[
宏规则组]
;
|{
宏规则组}
宏规则组 :
宏规则 (;
宏规则 )*;
?宏规则 :
宏匹配器=>
宏转录器宏匹配器 :
(
宏匹配*)
|[
宏匹配*]
|{
宏匹配*}
宏匹配 :
Token不包括$
和 定界符号 之外的任意 token
| 宏匹配器
|$
( 标识符或关键字 不包括crate
之外的任意标识符或关键字 | 原始标识符 |_
):
宏片段规格
|$
(
宏匹配+)
宏表示分割? 宏表示操作符宏片段规格 :
block
|expr
|ident
|item
|lifetime
|literal
|meta
|pat
|pat_param
|path
|stmt
|tt
|ty
|vis
宏表示分割 :
Token不包括 定界符号 和 宏表示操作符 之外的任意 token_宏表示操作符 :
*
|+
|?
宏转录器 :
定界Token树
允许用户以 macro_rules
声明的方式定义语法扩展。
这种语法扩展称为 "实例宏" ,或者简单的称为 "宏" 。
每个实例宏的定义都要有一个名称和一个以上 规则 。 每个规则都有两个部分:一个 匹配器 部分,用于描述匹配语法,另一个 转录器 部分,用于描述成功匹配并调用后将替换的语法。 匹配器和转录器都必须被包裹在定界符号中。 宏可以展开为表达式、语句、条目 (包括 trait 、实现和外部条目) 、类型或模式。
转录
当宏被调用时,宏展开器通过名称查找宏调用,并依次尝试匹配包含的宏规则。
在首个成功匹配处转录,如果导致错误,则不会尝试后续的规则。
在进行匹配时,不会对 Token 进行前瞻,如果编译器无法逐个明确如何解析,则会出错。
在下面的示例中,编译器不会前瞻标识符 $j ,不会前瞻是否已至 )
定界位置,虽然这能够更明确地解析:
#![allow(unused)] fn main() { macro_rules! ambiguity { ($($i:ident)* $j:ident) => { }; } ambiguity!(error); // Error: 局部歧义 }
在匹配器和转录器中, $
符号用于宏引擎实现调用的特定行为 (具体在 元变量 和 重复 中描述)。
非该符号标记的 Token 会按字面进行匹配和转录,特例是匹配器的外部定界符将匹配任意一对定界符,
例如,匹配器 (())
将匹配 {()}
,但是不匹配 {{}}
。字符 $
不能按字面匹配或转录。
转发匹配片段
在将匹配后的片段转发给内部的实例宏时,内部宏的匹配器将只能看到类型化的不透明 AST 片段。
内部宏无法简单通过字面 token 来匹配这个片段,只能使用相同类型规格的片段接收。
ident
、 lifetime
和 tt
片段类型例外,可以按照字面 token 的方式匹配。
以下是一个示例:
#![allow(unused)] fn main() { macro_rules! foo { // expr 表达式片段与 3 的字面值规则不匹配,虽然此表达式是 3 的字面值表达式。 ($l:expr) => { bar!($l); } // ERROR: ^^ 在宏调用中这个 token 没有预期的规则, } macro_rules! bar { (3) => {} } foo!(3); }
以下是在匹配 tt
Token 树片段后内部规则通过字面匹配 Token 的一个示例:
#![allow(unused)] fn main() { // 编译成功 macro_rules! foo { // `tt` Token 树片段直接作为字面 Token 与 3 的字面值表达式成功匹配。 ($l:tt) => { bar!($l); } } macro_rules! bar { (3) => {} } foo!(3); }
元变量
在匹配器中, $
名称 :
片段规格 定义元变量,用于匹配指定类型的语法片段,并与语法片段绑定。
有效的片段规格包括:
译注:这里 '片段规格' 的概念实际上与 '片段类型' 等价,使用规格这个词是与 rust 中的类型系统有所区分。
item
: 条目block
: 块表达式stmt
: 语句 不含尾部分号 (即,不包括需要分号的条目语句)pat_param
: 模式非顶层项pat
: 至少是任意 模式非顶层项, 更多取决于版次expr
: 表达式ty
: 类型ident
: 一个 标识符或关键字 或 原始标识符path
: 类型路径tt
: Token树 (简单 token 或匹配在定界符号()
、[]
、{}
中 token)meta
: 一个 Attr, 属性的内容lifetime
: 生命周期TOKENvis
: 一个可空的 可见性 限定词literal
: 匹配的-
?字面值表达式
元变量的片段类型在匹配器中已被指定,在转录器中可以用 $
名称 来引用。关键字元变量 $crate
用于引用当前的 crate。
转录器中元变量将替换为匹配到的语法片段。
可以将元变量多次转录,或者并不进行转录。
参阅下面的 卫生性 。
出于向后兼容的原因,单独的 _
下划线表达式 不会被 expr
片段匹配。当 _
做为子表达式时可被 expr
片段匹配。
版次差异: 从 2021 版本开始,
pat
规格片段可以匹配顶层或模式 (即它们接受 模式) 。在 2021 版本之前,则完全匹配与
pat_param
相同的片段 (即它们接受 模式非顶层项 ) 。这与
macro_rules!
定义生效的版次相关联。
重复
在匹配器和转录器中,重复语法是通过将要重复的 token 放在 $(
…)
中,后跟重复运算符,可选地包含分隔符 token 。
分隔符 token 可以是除定界符或重复运算符外的任意 token ,但常用的是 ;
和 ,
。
例如, $( $i:ident ),*
表明由逗号分隔的任意数量的标识符。重复允许嵌套。
重复运算符有:
*
— 表示任意数量的重复。+
— 表示任意数字,但至少是 1 。?
— 表示有 0 或 1 个产生的可选片段。
由于 ?
表示最多出现一次,所以它不能与分隔符一起使用。
重复的片段同时匹配和转录成指定数量的该片段,并由分隔符隔开。
元变量将匹配到对应片段的每个重复项。例如,上面示例的 $( $i:ident ),*
中元变量 $i
将匹配到列表中的所有标识符。
在转录过程中,重复操作受到额外的限制,以便能够正确地展开:
- 匹配器中的 '重复' ,在转录器中出现时,必须具有完全相同的数量、种类和嵌套顺序。
因此,对于匹配器中
$( $i:ident ),*
'重复' ,转录器中=> { $i }
、=> { $( $( $i)* )* }
和=> { $( $i )+ }
不一致的形式是不合法的, 对于=> { $( $i );* }
是正确的,这里只是将由逗号分隔的标识符列表替换为了分号分隔。 - 在转录器中,每个重复部分必须至少包含一个元变量,以决定展开它的次数。
如果同一个重复部分中出现多个元变量时,则它们绑定的片段数量必须相同。
例如,
( $( $i:ident ),* ; $( $j:ident ),* ) => (( $( ($i,$j) ),* ))
其$i
和$j
必须绑定相同数量的片段。 所以,以(a, b, c; d, e, f)
这样的内容调用宏是合法的,将展开为((a,d), (b,e), (c,f))
,但(a, b, c; d, e)
却不行,因为数量不同。 这个要求对于嵌套的重复同样适用。
作用域、导出和导入
由于历史原因,实例宏的作用域机制与条目并不完全相同。 宏有两种作用域形式:文本作用域和基于路径的作用域。 文本作用域是基于宏在源文件中的顺序,甚至会跨越多个文件,并且这是默认的作用域。在下面会进一步解释。 基于路径的作用域与条目的作用域完全相同。宏的作用域、导出和导入主要由属性控制。
当以未限定的标识符 (指宏不是多部分组成的路径的一部分) 形式调用宏时,则首先在文本作用域中查找宏。 如果未找到结果,那么将按路径作用域查找。 如果宏的名称带有路径限定符,则仅在基于路径的作用域中查找。
use lazy_static::lazy_static; // 基于路径导入。
macro_rules! lazy_static { // 定义文本。
(lazy) => {};
}
lazy_static!{lazy} // 首先在文本中找到了宏。
self::lazy_static!{} // 基于路径的查找忽略了文本中的宏,找到导入的宏。
文本作用域
文本作用域主要基于宏在源文件中出现的顺序,机制与 let
声明的局部变量的作用域类似,但宏文本作用域将应用于模块级别。
当使用 macro_rules!
定义了宏,则宏在定义的以下部分进入作用域。需注意的是,由于名称是从调用位置查找,因而可以递归。
在其围绕的作用域 (通常是模块) 关闭之前,宏可以进入子模块,甚至跨越多个文件:
//// src/lib.rs
mod has_macro {
// m!{} // Error: m 不在作用域内。
macro_rules! m {
() => {};
}
m!{} // OK: 出现在 m 的声明之后。
mod uses_macro;
}
// m!{} // Error: m 不在作用域内。
//// src/has_macro/uses_macro.rs
m!{} // OK: 在 src/lib.rs 中出现在 m 的声明之后。
多次定义宏不会产生错误,如果未超出作用域,最近一次的声明会隐藏先前的声明。
#![allow(unused)] fn main() { macro_rules! m { (1) => {}; } m!(1); mod inner { m!(1); macro_rules! m { (2) => {}; } // m!(1); // Error: 没有规则可以匹配 '1' m!(2); macro_rules! m { (3) => {}; } m!(3); } m!(1); }
宏也可以在函数内部声明和使用,工作方式与模块中声明类似:
#![allow(unused)] fn main() { fn foo() { // m!(); // Error: m 不在作用域内。 macro_rules! m { () => {}; } m!(); } // m!(); // Error: m 不在作用域内。 }
macro_use
属性
macro_use
属性 有两个用法。一是,应用于模块后,使模块中宏的作用域在模块关闭后仍不结束:
#![allow(unused)] fn main() { #[macro_use] mod inner { macro_rules! m { () => {}; } } // 这里使 m 的作用域延长到了模块之外 m!(); }
二是,应用于 crate 根模块中的 extern crate
声明,从而从另一个 crate 导入宏。
这种方式会将宏导入到 macro_use
预定义 中,而不是文本形式的导入,这表示可在任意位置隐藏此宏名称。
虽然 #[macro_use]
导入的宏可以在导入语句之前使用,但名称冲突时,最后导入的宏优先。
可选语法是,可以使用 [元标识符列表][MetaListIdents] 指定要导入的宏列表。当将 #[macro_use]
应用于模块时,不支持此功能。
#[macro_use(lazy_static)] // 仅 #[macro_use] 表示导入所有宏。
extern crate lazy_static;
lazy_static!{}
// self::lazy_static!{} // Error: lazy_static 在 `self` 未定义。
使用 #[macro_use]
导入的宏导出时必须使用 #[macro_export]
,下面将对此进行描述。
基于路径作用域
默认情况,宏没有基于路径的作用域。然而,如果赋予 #[macro_export]
属性,那么宏就被声明在 crate 根作用域内:
#![allow(unused)] fn main() { self::m!(); m!(); // OK: 基于路径方式查找,在当前模块中找到了 m 。 mod inner { super::m!(); crate::m!(); } mod mac { #[macro_export] macro_rules! m { () => {}; } } }
#[macro_export]
标记的宏总是 pub
的,可以通过路径或以上面描述的 #[macro_use]
方式引入其他 crate 中。
卫生性
默认情况下,在宏中引用的所有标识符都会按原样展开,并从宏调用的位置查找。
如果宏引用的条目或宏,在调用位置不在作用域内,则可能会出现问题。
为了缓解这个问题,可以在路径中使用 $crate
元变量,以强制在定义宏的 crate 内部进行查找。
//// 定义在 `helper_macro` crate 中。
#[macro_export]
macro_rules! helped {
// () => { helper!() } // 由于 'helper' 不在作用域中,这可能会导致错误。
() => { $crate::helper!() }
}
#[macro_export]
macro_rules! helper {
() => { () }
}
//// 在另一个 crate 里使用。
// 请注意,未导入 `helper_macro::helper` 。
use helper_macro::helped;
fn unit() {
helped!();
}
注意,因为 $crate
指的是当前的 crate ,所以当引用非宏条目时,必须使用完全限定的模块路径。
#![allow(unused)] fn main() { pub mod inner { #[macro_export] macro_rules! call_foo { () => { $crate::inner::foo() }; } pub fn foo() {} } }
此外,尽管 $crate
允许宏在展开时引用其所在 crate 内的条目,但并不会影响被引用项的可见性。
被引用的条目或宏必须从调用位置可见。
在下面的示例中,从 crate 外部尝试调用 call_foo!()
将失败,因为 foo()
不是公开的。
#![allow(unused)] fn main() { #[macro_export] macro_rules! call_foo { () => { $crate::foo() }; } fn foo() {} }
在 Rust 1.30 之前,不支持
$crate
和local_inner_macros
(下面会描述) 。 这两个语法与基于路径导入宏的语法一起添加,是为保证一些辅助宏不需要用户手动导入。 使用辅助宏的早期 Rust 版本编写的 crate 需要进行修改后,以使用$crate
或local_inner_macros
。
当一个宏被导出时,可以在 #[macro_export]
属性中添加 local_inner_macros
关键字,以自动在所有包含的宏调用中添加 $crate::
前缀。
这主要是为了迁移在 $crate
添加到语言之前编写的代码,使其能够与 Rust 2018 的基于路径的宏导入一起使用。
不建议在新代码中使用此功能。
#![allow(unused)] fn main() { #[macro_export(local_inner_macros)] macro_rules! helped { () => { helper!() } // 自动转换为 $crate::helper!() 。 } #[macro_export] macro_rules! helper { () => { () } } }
后继符冲突限制
宏系统使用的解析器相当强大,但出于防止当前或未来版本的语言产生歧义的考虑,对于宏有一些限制。 特别是,除了有关模糊展开的规则之外,由元变量匹配的非终端符号必须后跟一个 token ,此 token 已决定可以安全地在该类型的匹配之后使用。
举例来说,像 $i:expr [ , ]
这样的宏匹配器理论上是可以接受的,因为 [,]
不能成为合规的表达式的一部分,因此解析不会有歧义。
但是,由于 [
可以作为尾随的表达式的开始,因此 [
不是一个可以安全地在表达式之后排除的字符。
如果在以后的 Rust 版本中接受了 [,]
,这个匹配器就会变成有歧义或者解析错误,从而破坏工作中的代码。
对于,像 $i:expr,
或 $i:expr;
这样的匹配器是合规的,因为 ,
和 ;
是合规的表达式分隔符。
具体的规则如下:
expr
和stmt
后面只能跟着一个:=>
、,
、;
。pat_param
后面只能跟着一个:=>
、,
、=
、|
、if
、in
。pat
后面只能跟着一个:=>
、,
、=
、if
、in
。path
和ty
后面只能跟着一个:=>
、,
、=
、|
、;
、:
、>
、>>
、[
、{
、as
、where
, 或block
片段规格的元变量 。vis
后面只能跟着一个:,
, 一个非原始的priv
以外的标识符,任何可以开始一个类型的标记,或者一个带有ident
、ty
或path
片段规格的元变量。- 对所有其他片段规格没有限制。
版次差异: 在 2021 版之前,
pat
后面还可以跟着|
。
当涉及到重复时,规则适用于每次可能的展开,同时要考虑分隔符。这意味着:
- 如果重复包括一个分隔符,则该分隔符必须能够跟在重复内容之后。
- 如果重复可以重复多次 (
*
或+
) ,则其内容必须能够跟随它们自身。 - 重复的内容必须能够跟随之前的任何内容,而跟随重复内容的任何内容后面都必须能够跟随。
- 如果重复可以匹配零次 (
*
或?
) ,则后面必须能够跟随前面。
更多细节,见 形式说明 。