宏的示例
语法
宏规则定义 :
macro_rules
!
标识符 宏规则定义宏规则定义 :
(
宏规则)
;
|[
宏规则]
;
|{
宏规则}
宏规则 :
宏规则 (;
宏规则 )*;
?宏规则 :
宏匹配器=>
宏转换器宏匹配器 :
(
宏匹配*)
|[
宏匹配*]
|{
宏匹配*}
宏匹配 :
记号除了$
和 分隔符
| 宏匹配器
|$
( 标识符或关键字 除了crate
| 原始标识符 |_
):
宏片段说明符
|$
(
宏匹配+)
宏重复分隔符? 宏重复运算符宏片段说明符 :
block
|expr
|ident
|item
|lifetime
|literal
|meta
|pat
|pat_param
|path
|stmt
|tt
|ty
|vis
宏重复运算符 :
*
|+
|?
宏转换器 :
分隔记号树
macro_rules
允许用户以声明的方式定义语法扩展。我们称这种扩展为“示例宏”或简称“宏”。
每个示例宏都有一个名称和一个或多个规则。每个规则都有两部分:一个匹配器,描述它匹配的语法,以及一个转换器,描述将替换成功匹配的调用的语法。匹配器和转换器都必须用分隔符括起来。宏可以扩展为表达式、语句、项(包括特征、实现和外部项)、类型或模式。
转换
当调用宏时,宏扩展器会按名称查找宏调用,并依次尝试每个宏规则。它会转换第一个成功的匹配;如果这导致错误,则不会尝试后续匹配。匹配时,不执行预读;如果编译器无法明确地确定如何一次解析一个记号的宏调用,则这是一个错误。在以下示例中,编译器不会预读标识符以查看后面的记号是否是 )
,即使这将允许它明确地解析调用
#![allow(unused)] fn main() { macro_rules! ambiguity { ($($i:ident)* $j:ident) => { }; } ambiguity!(error); // Error: local ambiguity }
在匹配器和转换器中,$
记号用于从宏引擎调用特殊行为(在下面的 元变量 和 重复 中描述)。不属于此类调用的记号将按字面进行匹配和转换,但有一个例外。例外是匹配器的外部分隔符将匹配任何一对分隔符。因此,例如,匹配器 (())
将匹配 {()}
但不匹配 {{}}
。字符 $
不能按字面进行匹配或转换。
转发匹配的片段
将匹配的片段转发到另一个示例宏时,第二个宏中的匹配器将看到片段类型的不透明 AST。第二个宏不能使用文字记号来匹配匹配器中的片段,只能使用相同类型的片段说明符。ident
、lifetime
和 tt
片段类型是例外,并且可以由文字记号匹配。以下内容说明了此限制
#![allow(unused)] fn main() { macro_rules! foo { ($l:expr) => { bar!($l); } // ERROR: ^^ no rules expected this token in macro call } macro_rules! bar { (3) => {} } foo!(3); }
以下内容说明了如何在匹配 tt
片段后直接匹配记号
#![allow(unused)] fn main() { // compiles OK macro_rules! foo { ($l:tt) => { bar!($l); } } macro_rules! bar { (3) => {} } foo!(3); }
元变量
在匹配器中,$
名称 :
片段说明符 匹配指定类型的 Rust 语法片段,并将其绑定到元变量 $
名称。有效的片段说明符是
item
:一个 项block
:一个 代码块表达式stmt
:一个没有尾随分号的 语句(需要分号的项语句除外)pat_param
:一个 非顶层备选项模式pat
:至少任何 非顶层备选项模式,并且可能更多,具体取决于版本expr
:一个 表达式ty
:一个 类型ident
:一个 标识符或关键字 或 原始标识符path
:一个 类型路径 风格的路径tt
:一个 记号树 (单个 记号 或匹配分隔符()
、[]
或{}
中的记号)meta
:一个 属性,属性的内容lifetime
:一个 生命周期记号vis
:一个可能为空的 可见性 限定符literal
:匹配-
?字面量表达式
在转换器中,元变量仅由 $
名称 引用,因为片段类型在匹配器中指定。元变量将替换为与其匹配的语法元素。关键字元变量 $crate
可用于引用当前包;请参阅下面的 卫生宏。元变量可以被转换多次或根本不被转换。
出于向后兼容性的原因,尽管 _
也是一个表达式,但独立的下划线与 expr
片段说明符不匹配。但是,当 _
作为子表达式出现时,它与 expr
片段说明符匹配。
版本差异:从 2021 版开始,
pat
片段说明符匹配顶层或模式(也就是说,它们接受 模式)。在 2021 版之前,它们匹配与
pat_param
完全相同的片段(也就是说,它们接受 非顶层备选项模式)。相关版本是
macro_rules!
定义生效的版本。
重复
在匹配器和转换器中,重复通过将要重复的记号放在 $(
…)
中来表示,后跟一个重复运算符,可选地在两者之间使用分隔符记号。分隔符记号可以是除分隔符或重复运算符之外的任何记号,但 ;
和 ,
最常见。例如,$( $i:ident ),*
表示由逗号分隔的任意数量的标识符。允许嵌套重复。
重复运算符是
*
- 表示任意次数的重复。+
- 表示至少一次的重复。?
- 表示可选片段,出现零次或一次。
由于 ?
最多表示出现一次,因此不能与分隔符一起使用。
重复片段匹配并转录为指定数量的片段,并由分隔符标记分隔。元变量与其对应片段的每次重复匹配。例如,上面的 $( $i:ident ),*
示例将 $i
与列表中的所有标识符匹配。
在转录过程中,对重复应用了额外的限制,以便编译器知道如何正确扩展它们。
- 元变量在转录器中的出现次数、种类和嵌套顺序必须与其在匹配器中的完全相同。因此,对于匹配器
$( $i:ident ),*
,转录器=> { $i }
、=> { $( $( $i)* )* }
和=> { $( $i )+ }
都是非法的,但=> { $( $i );* }
是正确的,它将逗号分隔的标识符列表替换为分号分隔的列表。 - 转录器中的每个重复必须至少包含一个元变量,以确定要扩展多少次。如果多个元变量出现在同一个重复中,则它们必须绑定到相同数量的片段。例如,
( $( $i:ident ),* ; $( $j:ident ),* ) => (( $( ($i,$j) ),* ))
必须绑定与$j
片段相同数量的$i
片段。这意味着使用(a, b, c; d, e, f)
调用宏是合法的,并扩展为((a,d), (b,e), (c,f))
,但(a, b, c; d, e)
是非法的,因为它没有相同数量的片段。此要求适用于嵌套重复的每一层。
作用域、导出和导入
由于历史原因,示例宏的作用域与项的作用域并不完全相同。宏有两种形式的作用域:文本作用域和基于路径的作用域。文本作用域基于事物在源文件中的出现顺序,甚至跨多个文件,并且是默认作用域。下面将对此进行进一步说明。基于路径的作用域的工作方式与项作用域完全相同。宏的作用域、导出和导入主要由属性控制。
当通过非限定标识符(不是多部分路径的一部分)调用宏时,首先在文本作用域中查找它。如果这没有产生任何结果,则在基于路径的作用域中查找它。如果宏的名称使用路径限定,则仅在基于路径的作用域中查找它。
use lazy_static::lazy_static; // Path-based import.
macro_rules! lazy_static { // Textual definition.
(lazy) => {};
}
lazy_static!{lazy} // Textual lookup finds our macro first.
self::lazy_static!{} // Path-based lookup ignores our macro, finds imported one.
文本作用域
文本作用域主要基于事物在源文件中的出现顺序,其工作方式类似于使用 let
声明的局部变量的作用域,但它也适用于模块级别。当使用 macro_rules!
定义宏时,宏在定义后进入作用域(请注意,它仍然可以递归使用,因为名称是从调用站点查找的),直到其周围的作用域(通常是模块)关闭。这可以进入子模块,甚至可以跨越多个文件。
//// src/lib.rs
mod has_macro {
// m!{} // Error: m is not in scope.
macro_rules! m {
() => {};
}
m!{} // OK: appears after declaration of m.
mod uses_macro;
}
// m!{} // Error: m is not in scope.
//// src/has_macro/uses_macro.rs
m!{} // OK: appears after declaration of m in src/lib.rs
多次定义宏不是错误;除非前一个声明超出作用域,否则最近的声明将隐藏前一个声明。
#![allow(unused)] fn main() { macro_rules! m { (1) => {}; } m!(1); mod inner { m!(1); macro_rules! m { (2) => {}; } // m!(1); // Error: no rule matches '1' m!(2); macro_rules! m { (3) => {}; } m!(3); } m!(1); }
宏也可以在函数内部本地声明和使用,并且工作方式类似。
#![allow(unused)] fn main() { fn foo() { // m!(); // Error: m is not in scope. macro_rules! m { () => {}; } m!(); } // m!(); // Error: m is not in scope. }
macro_use
属性
macro_use
属性有两个用途。首先,它可以用于使模块的宏作用域在模块关闭时不结束,方法是将其应用于模块。
#![allow(unused)] fn main() { #[macro_use] mod inner { macro_rules! m { () => {}; } } m!(); }
其次,它可以用于从另一个 crate 导入宏,方法是将其附加到出现在 crate 根模块中的 extern crate
声明。以这种方式导入的宏将导入到 macro_use
前奏 中,而不是文本形式,这意味着它们可以被任何其他名称遮蔽。虽然由 #[macro_use]
导入的宏可以在 import 语句之前使用,但在发生冲突的情况下,最后导入的宏获胜。可以选择使用 MetaListIdents 语法指定要导入的宏列表;当 #[macro_use]
应用于模块时,不支持此功能。
#[macro_use(lazy_static)] // Or #[macro_use] to import all macros.
extern crate lazy_static;
lazy_static!{}
// self::lazy_static!{} // Error: lazy_static is not defined in `self`
要使用 #[macro_use]
导入的宏必须使用 #[macro_export]
导出,如下所述。
基于路径的作用域
默认情况下,宏没有基于路径的作用域。但是,如果它具有 #[macro_export]
属性,则它在 crate 根作用域中声明,并且可以像这样正常引用。
#![allow(unused)] fn main() { self::m!(); m!(); // OK: Path-based lookup finds m in the current module. mod inner { super::m!(); crate::m!(); } mod mac { #[macro_export] macro_rules! m { () => {}; } } }
标有 #[macro_export]
的宏始终是 pub
的,并且可以被其他 crate 引用,可以通过路径或通过上述 #[macro_use]
引用。
卫生
默认情况下,宏中引用的所有标识符都按原样扩展,并在宏的调用站点查找。如果宏引用了调用站点不在作用域中的项或宏,则可能会导致问题。为了缓解这种情况,可以在路径的开头使用 $crate
元变量来强制在定义宏的 crate 内部进行查找。
//// Definitions in the `helper_macro` crate.
#[macro_export]
macro_rules! helped {
// () => { helper!() } // This might lead to an error due to 'helper' not being in scope.
() => { $crate::helper!() }
}
#[macro_export]
macro_rules! helper {
() => { () }
}
//// Usage in another crate.
// Note that `helper_macro::helper` is not imported!
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
(如下)。它们与基于路径的宏导入(如上所述)一起添加,以确保宏导出 crate 的用户不需要手动导入辅助宏。为早期版本的 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!() } // Automatically converted to $crate::helper!(). } #[macro_export] macro_rules! helper { () => { () } } }
后续集歧义限制
宏系统使用的解析器相当强大,但为了防止当前或未来版本的语言出现歧义,它受到限制。特别是,除了关于歧义扩展的规则之外,由元变量匹配的非终结符后面必须跟着一个已被确定可以在该类匹配之后安全使用的标记。
例如,像 $i:expr [ , ]
这样的宏匹配器在理论上可以在今天的 Rust 中被接受,因为 [,]
不能是合法表达式的一部分,因此解析将始终是明确的。但是,因为 [
可以开始尾随表达式,所以 [
不是一个可以安全地排除在表达式之后出现的字符。如果在以后版本的 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
后面还可以跟|
。
当涉及重复时,规则将应用于所有可能的扩展次数,并考虑分隔符。这意味着:
- 如果重复包含分隔符,则该分隔符必须能够跟在重复的内容之后。
- 如果重复可以重复多次(
*
或+
),则内容必须能够跟在自身之后。 - 重复的内容必须能够跟在前面内容的后面,并且后面内容必须能够跟在重复的内容之后。
- 如果重复可以匹配零次(
*
或?
),则后面内容必须能够跟在前面内容的后面。
有关更多详细信息,请参阅正式规范。