宏
输入语法与输出语法一致
Input syntax is evocative of the output (C-EVOCATIVE)
Rust 宏系统允许你创造出你想要的输入语法。 尽量让输入的语法是让大多数人感到熟悉的, 仿照现存的 Rust 语法以便让使用者的代码风格统一。 注意对关键字和标点符号的选择与更改。
好的标准是:宏输入的语法与输出的结果不会相差太大,尤其在关键字和标点符号方面。
比如你写的宏要根据输入来声明一个具体的结构体,那么就以 struct
关键字开头,
后接结构体的名称。
// 这种方式更好
bitflags! {
struct S: u32 { /* ... */ }
}
// 这种方式就不太好
bitflags! {
S: u32 { /* ... */ }
}
// 这种方式也不太好
bitflags! {
flags S: u32 { /* ... */ }
}
另一个例子是分号和逗号之间如何做出选择。 Rust 的常量 (constants) 以分号作为结尾,所以如果你写的宏要定义一系列常量, 那么应该在输入语法里书写分号,即使语法看起来与 Rust 的语法有些不同。
// 原始的常量定义语法后面接的是分号
const A: u32 = 0b000001;
const B: u32 = 0b000010;
// 所以这种方式更好
bitflags! {
struct S: u32 {
const C = 0b000100;
const D = 0b001000;
}
}
// 这种方式不太好
bitflags! {
struct S: u32 {
const E = 0b010000,
const F = 0b100000,
}
}
宏是多样化的,以致于这里举的例子不会适合所有场景。 但是的确要仔细思考如何应用同一套规则。
宏与属性形成有机的整体
Item macros compose well with attributes (C-MACRO-ATTR)
生成超过一个条目 (item) 的宏,应该支持对每个条目添加属性。 一个常见的例子是,把单独的条目放在 cfg 后面:
bitflags! {
struct Flags: u8 {
#[cfg(windows)]
const ControlCenter = 0b001;
#[cfg(unix)]
const Terminal = 0b010;
}
}
生成结构体或枚举体的宏,应该支持添加宏属性, 方便让输出结果使用 derive 属性。
bitflags! {
#[derive(Default, Serialize)]
struct Flags: u8 {
const ControlCenter = 0b001;
const Terminal = 0b010;
}
}
生成条目的宏可以在条目被允许的地方使用
Item macros work anywhere that items are allowed (C-ANYWHERE)
Rust 允许条目 (item) 有模块级别那样的作用域或者出现在类似函数这样小的作用域里。 生成条目的宏应该和普通的条目一样,在这些地方可以正常使用。 测试这些宏的时,应该至少在模块和函数作用域里都调用宏。
#[cfg(test)]
mod tests {
test_your_macro_in_a!(module);
#[test]
fn anywhere() {
test_your_macro_in_a!(function);
}
}
下面这个例子展示了错误的宏: 这个宏能在模块作用域里运行,但不能在函数作用域里运行。
macro_rules! broken {
($m:ident :: $t:ident) => {
pub struct $t;
pub mod $m {
pub use super::$t;
}
}
}
broken!(m::T); // 运行通过,可以展开成 T 和 m::T
fn g() {
broken!(m::U); // 编译失败,super::U 指的是上级模块,而不是函数 g
}
生成条目的宏应支持可见性分类符
Item macros support visibility specifiers (C-MACRO-VIS)
宏应遵循 Rust 对条目可见性 (visibility) 的语法要求:
默认是私有的,如果使用 pub
则表明条目是公有的。
bitflags! {
struct PrivateFlags: u8 {
const A = 0b0001;
const B = 0b0010;
}
}
bitflags! {
pub struct PublicFlags: u8 {
const C = 0b0100;
const D = 0b1000;
}
}
类型分类符 $t:ty
是灵活的
Type fragments are flexible (C-MACRO-TY)
如果你写的宏接收 $t:ty
类型 分类符 作为输入,
那么输入的内容应该与以下代码一起使用:
- 原生类型:
u8
,&str
- 相对路径:
m::Data
- 绝对路径:
::base::Data
- 上级相对路径:
super::Data
- 泛型:
Vec<String>
下面这个例子展示了错误的宏: 这个宏能在很好地与原生类型、绝对路径一起使用, 但不能和相对路径一起使用。
#![allow(unused)] fn main() { macro_rules! broken { ($m:ident => $t:ty) => { pub mod $m { pub struct Wrapper($t); } } } broken!(a => u8); // okay broken!(b => ::std::marker::PhantomData<()>); // okay struct S; broken!(c => S); // 编译失败: `S` not found in this scope }