
在上一篇文章中,我们介绍了什么是声明式宏,以及如何定义和使用它们。
在本文中,我们将重点关注如何把它们集成到你的 crate 中。这意味着我们会涵盖宏的作用域、组织和导出。我们还会通过示例来看一些常见的用例。
希望读完这两篇文章后,你会觉得已经准备好开始在自己的 crate 中尝试声明式宏了!
本文在 Rust 2024 edition 下测试。对于较旧的代码,你可能会看到一些
// You cannot access this macro outside of the function.
如果这个宏会在同一模块内的多个项中使用,你可以直接在模块本身中定义它。这比在单个函数内定义可见性更高,但它仍然只限于当前模块及其子模块。
// You cannot use it here, because it's not defined yet.
macro_rules! helper_macro { () => {}; }
// Now it can be invoked and used. helper_macro!();
// It can be used within functions, as one might expect. fn within_functions() { helper_macro!(); }
// Works within submodules of any depth as well, // which might feel counter-intuitive. mod even_within_submodules { helper_macro!(); }
有时,从包含模块所属的父模块中使用宏会很有帮助。
现代 Rust 语法通过基于路径的作用域更好地支持这一点,我们将在下一节了解它。
话虽如此,你可能仍会遇到这种语法,所以我们应该介绍它。
要把子模块中定义的宏提升到父级的文本作用域中,你可以在容纳该子模块的模块上附加 #[macro_use] 属性。
// We still cannot use helper_macro! here, it isn't defined yet.
// After the module definition completes, // it's now defined in the parent's textual scope. #[macro_use] mod submodule { macro_rules! helper_macro { () => {}; } helper_macro!(); }
// See? Now we can access and use it here even without an import. // It acts as-if we had defined textually after submodule. helper_macro!();
// Works within submodules of any depth as well, // since it's a part of the textual scope. mod sibling_submodule { helper_macro!(); }
但请注意,#[macro_use] 不会自动向模块树更上层传播。
如果你希望宏一路向上都在模块树中保持定义,就必须继续在所有父模块上附加 #[macro_use] 属性。
#[macro_use] // <- Necessary if you need it... mod outer_file { #[macro_use] mod other_file { macro_rules! helper_macro { () => {}; } helper_macro!(); } helper_macro!(); } helper_macro!(); // <- ... at this location onwards.
> 历史上,#[macro_use] 也曾用于把来自其他 crate 的外部宏导入到你的文本作用域中。然而,对于从其他 crate 导入的宏,你不应再这样做。
历史上,#[macro_use] 也曾用于把来自其他 crate 的外部宏导入到你的文本作用域中。
然而,对于从其他 crate 导入的宏,你不应再这样做。
### 基于路径的作用域规则
声明式宏的基于路径的作用域让我们能以更符合习惯的方式使用宏。
- 宏的工作方式很像任何其他项,并且可以在定义之前调用。
- 为了使用它,需要手动导入它,或从模块树中解析它。
这可以通过紧跟在宏定义之后的一小段代码来完成:
macro_rules! helper_macro { () => {}; }
// Re-export as an item in the module tree. pub(crate) use helper_macro;
这样做之后,由于宏现在是模块树的一部分,顺序不再重要。
// Can call here, even if the macro isn't defined yet. helper_macro!();
macro_rules! helper_macro { () => {}; } pub(crate) use helper_macro;
// Can obviously still call here. helper_macro!();
> 尽管当宏使用基于路径的作用域时,你可以不按顺序引用它们,但在同一个文件中,你大概仍会希望在使用它们之前先定义它们。不是因为必须这样做,而是因为这样更易读!
真正的益处在于,其他模块可以像导入和使用任何其他项一样导入并使用这个宏。
mod submodule { macro_rules! helper_macro { () => {}; } pub(crate) use helper_macro; }
文章标题:Rust 声明式宏:组织、作用域和常见用例
正文:
mod another_submodule {
// 你可以像普通项一样使用它!
use crate::submodule::helper_macro;
helper_macro!();
// 或者你可以通过路径引用它!
crate::submodule::helper_macro!();
}
最常见的是将其与 pub(crate) 一起使用,但实际上你可以在这里使用所有项的可见性(pub(super)、pub(in some::path)、空表示私有)。然而,pub 有一些奇怪的性质——我们将在下一节中讨论。
导出声明式宏
如果你想让声明式宏在你的 crate 外部可用,你必须给它附加 #[macro_export] 属性。
决定导出宏时要小心!
在你导出宏之后,用户可能会逐渐依赖它,这可能使其难以更改或维护。这就是为什么我通常倾向于不导出宏,直到我确信有必要。
不过,如果你确定这是正确的做法,你可以像这样实现:
#[macro_export]
macro_rules! exported_macro {
() => {};
}
mod submodule {
use crate::exported_macro;
exported_macro!();
}
此属性的可观察效果是:
- 你的 crate 根将公开导出该宏供依赖者使用(use your_crate::exported_macro)。
- 你可以使用基于路径的常规作用域私有导入该宏(use crate::exported_macro)。
宏是否在子模块中并不重要,它始终会在根处导出。
exported_macro!(); // <- 因为 `#[macro_export]` 而有效
mod module {
use crate::exported_macro;
exported_macro!(); // <- 因为 `use` 语句而有效
mod submodule {
#[macro_export]
macro_rules! exported_macro {
() => {};
}
}
exported_macro!(); // <- 因为 `use` 语句而有效
}
exported_macro!(); // <- 因为 `#[macro_export]` 而有效
#[macro_use] 属性在你的 crate 内部仍然有效,正如人们对文本作用域规则的预期一样。不过,我不建议在包含导出宏的模块上使用它,而是直接通过导出的路径来使用。
exported_macro!(); // <- 因为 `#[macro_export]` 而有效
mod module {
// <- 没有 `use` 语句在这里将无法使用
#[macro_use]
mod submodule {
#[macro_export]
macro_rules! exported_macro {
() => {};
}
}
exported_macro!(); // <- 因为 `#[macro_use]` 而有效
}
exported_macro!(); // <- 因为 `#[macro_export]` 而有效
如果你想在 crate 根之外的其他地方导出宏,有一种方法可以实现。
如果能够不使用 #[macro_export],而是使用 pub use exported_macro 就好了,但截至目前这不起作用。如果你尝试这样做,你会遇到一个编译错误,提示该宏“仅在 crate 内公开,无法重新导出”。
因此,你必须在导出的宏上使用 #[macro_export]。然后,你可以使用 pub use exported_macro 将该导出的宏绑定到模块树中的特定位置。
这很烦人,因为它会在根处发布一个宏(你可能并不想这样),所以对此的通用模式是:
- 隐藏公开导出的宏(这样人们就看不到它实际上在根处)。
- 在你希望访问该宏的模块中创建一个公开的路径绑定。
例如:
pub mod submodule {
// 由于这是 `#[macro_export]`,
// 它仍然会出现在 crate 根处。
//
// 因此,我们以一种阻止使用的方式命名它。
// 我们还将其隐藏,使其不出现在文档中。
#[doc(hidden)]
#[macro_export]
macro_rules! __exported_macro {
() => {};
}
/// 实际的文档将放在这里!
#[doc(inline)]
pub use __exported_macro as exported_macro;
}
// 严格来说,这确实导出了以下宏……
crate::__exported_macro!();
// However, documentation will only list this version. // And, this version will be public for dependents to use. crate::submodule::exported_macro!();
> 在 Rust 的未来版本中,围绕可见性和模块树导出的这些冗余可能会被清理掉。对 Rust 语言做这类不破坏现有代码的改动很难。所以这仍然是语言中那些奇怪的部分之一——如果不小心,它可能会以古怪、不符合习惯的方式工作。Rust internals forum 上有一个关于此事的相当深入的讨论帖。
在 Rust 的未来版本中,围绕可见性和模块树导出的这些冗余可能会被清理掉。
对 Rust 语言做这类不破坏现有代码的改动很难。所以这仍然是语言中那些奇怪的部分之一——如果不小心,它可能会以古怪、不符合习惯的方式工作。
Rust internals forum 上有一个关于此事的相当深入的讨论帖。
## 关于宏卫生的提醒
回顾我们之前那篇关于宏卫生的文章,当时我们谈到 Rust 使用所谓的“mixed-site hygiene”。
大多数项——除局部变量之外——都从宏被调用的位置解析(函数、类型、trait 等)。对于
- 对于在多个模块之间共享的宏,将它们定义在一个公共父模块中。提示:对于较小的 crate,通常最容易的做法是以最顶层的模块(lib.rs)为目标。
- 提示:对于较小的 crate,通常最容易的做法是以最顶层的模块(lib.rs)为目标。
我不喜欢随意把宏塞进某个父模块(或者我有好几个这样的模块,那会变得难以管理)。所以我会为我定义的每个宏把它们放到单独的文件中,然后把它们重新导出到某个用于组织的父模块中。
crate 导出的宏很适合这种方式。在那些情况下,如果访问该宏的预期方式是从 crate 根访问,我就不会重新导出到父模块中。
> 这要追溯到我那篇关于模块组织的文章,其中我详细介绍了使用“组织模块”的方法,以及在各自单独的文件中定义类型。如果你在理解我的组织策略时遇到困难,请去看看那篇文章。
这要追溯到我那篇关于模块组织的文章,其中我详细介绍了使用“组织模块”的方法,以及在各自单独的文件中定义类型。
如果你在理解我的组织策略时遇到困难,请去看看那篇文章。
mod.rs:(组织模块;通常是 lib.rs)
mod macros; mod type_a; mod type_b; mod type_c; // etc...
macros/mod.rs:(组织模块)
// Modules: mod exported_macro; mod helper_macro_a; mod helper_macro_b; mod helper_macro_c; // etc...
// Re-Exports: pub(crate) use helper_macro_a::; pub(crate) use helper_macro_b::; pub(crate) use helper_macro_c::*; // etc...
macros/exported_macro.rs:
// In this case, I'm happy to bind it to the root. #[macro_export] macro_rules! exported_macro { () => {}; }
macros/helper_macro_a.rs:
macro_rules! helper_macro_a { () => {}; } pub(crate) use helper_macro_a;
在 crate 的其他地方:
// Either pull them in for usage: use crate::macros::{helper_macro_a, helper_macro_b}; helper_macro_a!(); helper_macro_b!();
// Or refer to them via absolute paths: crate::macros::helper_macro_c!();
我知道这看起来有点小题大做,但这样做的好处是,我总是能非常容易地找到一个宏。要么宏定义在它使用位置的附近(也就是在同一个文件内),要么文件与宏同名,因此可以通过命令轻松打开。
如果一个宏与另一个宏关系非常密切,而且它们被拆分开只是为了便于维护,我通常会把它们放在同一个文件里。但只有在它们本意是作为要调用的主宏的实现细节时,我才会这样做。
而且,你并不总是需要一个用于组织宏的模块——你完全可以直接把宏重新导出到主组织模块中。这由每个人和每个项目自行决定。
## 声明式宏的用例
既然我们已经知道如何将声明式宏集成到项目中,接下来看看它们的一些用例。
冒着听起来像老生常谈的风险——我想提醒你,要尽可能努力地不要导出宏。
如前所述,crate 本地宏真的很好,因为用户只依赖它的副作用。你完全控制语法和它被调用的位置。关于破坏性变更,或者对宏非预期用法的依赖,需要担心的事情更少。
只有当你确定这是一个明智的决定时,才导出宏。
## 实现 Trait 和函数
在开发一个 crate 时,你可能会编写一些类型,它们具有某些实现几乎相同的函数或 trait。
如果这些类型在语义上密切相关,我们可以使用辅助宏来减少重复。辅助宏将根据提供给它的一些信息来实现共同的 trait 或函数。
一些具体的有益示例包括:
- 如果你需要编写几个类型,它们大多是对其他类型的简单包装(类似于 newtype 的东西)。
Rust 声明式宏:组织、作用域与常见用例
- 如果你想确保多个类型拥有相同的 API 子集(函数和 trait)。
不过,要注意不要一出现最轻微的重复迹象就动用声明式宏。相反,只有当内容看起来非常像复制粘贴时,你才应该考虑它。如果你过度泛化,这可能会让维护成为更大的负担,而不是更小。
对于这类实现宏,我喜欢遵循 key = value 模式。
macro_rules! impl_example { ( input_1 = $input_1:literal, input_2 = ($($input_2:tt)), input_3 = { grouped_input_1 = $grouped_input_1:literal, grouped_input_2 = $grouped_input_2:ident, grouped_input_3 = $grouped_input_3:expr, }, input_4 = [$($input_4:expr), $(,)?], ) => { /Expansion Here/ }; }
impl_example! { input_1 = 123, input_2 = (complex; grouped, input), input_3 = { grouped_input_1 = "sample", grouped_input_2 = Sample, grouped_input_3 = concat!("..."), // ... }, input_4 = [ array_value_1, array_value_2, array_value_3, // ... ], // ... etc ... }
我偏好这种语法的原因是:
- 你不会在不被调用处看见的情况下,不小心把某个片段映射到错误的输入。
- 它使用与结构体初始化不同的绑定语法,以便与“看起来像是在初始化对象”保持距离。
- 尽管绑定语法不同,但它感觉足够熟悉,能让人立刻理解。
不过,我承认最后两点没那么重要。如果你觉得更舒服,也可以使用更接近“结构体字段初始化”风格的格式(key: value)。这由你决定!
> 不要过度泛化这些内容,按一种顺序接受它们——毕竟我们只是在内部使用它们。我有时喜欢按键名进行字母数字排序,这样至少顺序是一致的。
不要过度泛化这些内容,按一种顺序接受它们——毕竟我们只是在内部使用它们。
我有时喜欢按键名进行字母数字排序,这样至少顺序是一致的。
现实中的一个例子是 Rust 语言本身的整数类型。整数类型(如 u8)使用 crate 局部的 int_impl 宏来实现通用函数和 trait。
使用声明式宏来协助在不同元数的元组上实现某个 trait 也很常见。这通常适用于你的 trait 只会在所有元组元素上调用同一个 trait 的情况。你可以在 serde crate 中看到一个例子:crate 局部的 tuple_impls! 宏。
如果你想要一个玩具示例——这里有一个使用一些强类型时间值的例子。该宏帮助我们确保不同时间类型之间的转换实现保持一致。
#[derive(Copy, Clone, Debug, PartialEq)] pub struct OverflowError;
#[derive(Copy, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] pub struct Hours(u32);
#[derive(Copy, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] pub struct Minutes(u32);
#[derive(Copy, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] pub struct Seconds(u32);
macro_rules! impl_time_functionality { ( from = [$($from:ty), $(,)?], seconds_per = $seconds_per:expr, try_from = [$($try_from:ty), $(,)?], type = $type:ty, ) => { // Ensure that we have common functions that we expect all of these time types to have. // // Reminder that you can open multiple impl blocks for a single type, so if there are // functions that don't make sense to be commonly implemented here, then just write them // in a separate impl block for the intended type outside of this macro. impl $type { // Helper constant to facilitate conversion impls. // We don't want to export it publicly from this case. pub(crate) const SECONDS_PER: u32 = $seconds_per;
pub fn to_u32(self) -> u32 { self.0 } }
$( impl TryFrom<$try_from> for $type { type Error = OverflowError; fn try_from(value: $try_from) -> Result<Self, Self::Error> { // This const assert ensures we haven't used the helper macro improperly. const _: () = assert!( <$try_from>::SECONDS_PER > $seconds_per, concat!(stringify!($try_from), " must contain more seconds than ", stringify!($type)), ); Ok(Self(value.to_u32() .checked_mul(<$try_from>::SECONDS_PER / Self::SECONDS_PER) .ok_or(OverflowError)?)) } } )*
$( impl From<$from> for $type { fn from(value: $from) -> Self { // This const assert ensures we haven't used the helper macro improperly. const _: () = assert!( <$from>::SECONDS_PER < $seconds_per, concat!(stringify!($from), " must contain fewer seconds than ", stringify!($type)), ); Self(value.to_u32() / (Self::SECONDS_PER / <$from>::SECONDS_PER)) } } )* }; }
impl_time_functionality! { from = [], seconds_per = 1, try_from = [ Minutes, Hours ], type = Seconds, }
impl_time_functionality! { from = [ Seconds ], seconds_per = 60, try_from = [ Hours ], type = Minutes, }
impl_time_functionality! { from = [ Seconds, Minutes ], seconds_per = 60 * 60, try_from = [], type = Hours, }
#[test] fn test() { assert_eq!(Hours::from(Seconds(60 60)), Hours(1)); assert_eq!(Minutes::from(Seconds(60 60)), Minutes(60)); assert_eq!(Seconds::from(Seconds(60 60)), Seconds(60 60)); assert_eq!(Hours::from(Minutes(60)), Hours(1)); assert_eq!(Minutes::from(Minutes(60)), Minutes(60)); assert_eq!(Seconds::try_from(Minutes(60)), Ok(Seconds(60 60))); assert_eq!(Hours::from(Hours(1)), Hours(1)); assert_eq!(Minutes::try_from(Hours(1)), Ok(Minutes(60))); assert_eq!(Seconds::try_from(Hours(1)), Ok(Seconds(60 60))); }
> 有时去重并不值得以牺牲可读性为代价,但这需要视具体情况而定。对于像这样的简单类型(以及标准库中的整数类型),好处是显而易见的,因为它能产生明显的预期最终结果,并确保所有 API 都以统一的方式实现。如果你发现自己添加了大量特殊处理,并将宏拆分成许多独立的宏,这可能表明你试图在一个不适合的地方使用这种模式。在这些情况下,最好直接手写所有 trait 和实现,即使这会导致一些重复。
有时去重并不值得以牺牲可读性为代价,但这需要视具体情况而定。对于像这样的简单类型(以及标准库中的整数类型),好处是显而易见的,因为它能产生明显的预期最终结果,并确保所有 API 都以统一的方式实现。
如果你发现自己添加了大量特殊处理,并将宏拆分成许多独立的宏,这可能表明你试图在一个不适合的地方使用这种模式。
在这些情况下,最好直接手写所有 trait 和实现,即使这会导致一些重复。
## 类似 format! 的表达式
声明式宏的一个非常流行的用途是匹配标准库中的 format! 语法。
你并不总是需要提供这样的宏,有时你只需接受 impl Into<String>,让用户按自己的喜好格式化。但在用户总是要格式化字符串的情况下,或者当你希望在禁用 alloc 支持时把格式化条件编译掉时,它可以让代码更具可读性。
再次以标准库为例,我们可以看看 todo! 宏。它足够简单,所以就直接在那里实现为声明式宏,而不是像许多其他宏(类似于 assert_eq!、matches! 以及其他一些宏)那样作为内建宏。
这有点像是唾手可得的例子,但 log crate 有一些相当明显的例子:info!、warn! 和 error!。一般来说,任何带有类似 format! 操作的场景都能从声明式宏中受益。
类似 assert! 的宏是这类宏的另一个例子。你可能不会想太多,但为单元测试的失败提供额外细节会很好。assert! 宏都在末尾接受格式化参数,如果你需要某种适配的 assert! 宏,你也可以这样做。
macro_rules! assert_in_range { ($input:expr, $low:expr, $high:expr, $($msg:tt)) => {{ let input = $input; let low = $low; let high = $high; assert!( input >= $low, "{}: range check failed: {input} >= {low}", format_args!($($msg)), ); assert!( input <= $high, "{}: range check failed: {input} <= {high}", format_args!($($msg)*), ); }}; }
#[test] fn example() { let number = 5; assert_in_range!(number, 50, 100, "formatted example (input={})", number); }
这里的失败看起来像这样:
thread 'example' (50) panicked at src/lib.rs:22:5: formatted example (input=5): range check failed: 5 >= 50 note: run with RUST_BACKTRACE=1 environment variable to display a backtrace
## 用于初始化复杂类型
对于许多类型,你可以只使用结构体初始化写法,或者使用构建器模式。
#[derive(Default)] struct Example { foo: u8, bar: String, baz: bool, }
// Some initialization patterns: fn struct_initialization() { let object = Example { foo: 0, bar: format!("example formatted string"), baz: false, }; }
fn struct_initialization_partial() { let object = Example { bar: format!("example formatted string"), ..Example::default() }; }
fn builder_pattern() { // ExampleBuilder type not shown for brevity. let object = ExampleBuilder::default() .with_foo(123) .with_bar(format!("example formatted string")) .finish(); }
但有时出于可读性原因,我们希望使用与此不同的语法。
通常,自定义语法只有在存在某种常见、被广泛接受且预先存在的格式(例如像 JSON 这样的现有数据标记语言)时才有益。但偶尔,纯粹自定义的语法也会有帮助。
容器是自定义初始化语法的一个很好的例子,每个人都应该对此有一些经验。
确实,你可以(也应该)实现 FromIterator 来从某个元素序列初始化。但对于并非来自某些已构造值的显式初始化,这种语法可能会很别扭。因此,为了开发者方便,我们常常创建小型辅助宏来提高可读性。
就像与宏相关的许多事情一样,只有当你确定它们有用时才添加它们。想一想现有的对象初始化方法是否已经足够好。
不言而喻,vec! 宏是这方面非常受欢迎的例子。很快我们还会拥有它的另一个版本,即 hash_map! 宏。
对于对象初始化,我能想到的最好的例子是 serde_json crate 中的 json! 宏。
Rust 声明式宏:组织、作用域与常见用例
这实际上是一个使用声明式宏完成非平凡工作的好例子。具体来说,注意它们通过将宏拆分为多个宏来隐藏实现细节,json! 是你调用的主要宏,而它会重定向到另一个导出的宏,该宏在文档中隐藏,名为 json_internal。
不过,我承认,json! 是我认为徘徊在“本应是过程宏”边缘的声明式宏之一。不过,此时要安全地将其改过来可能很难。
## 减少样板代码
有时你会发现自己在少量函数中写了许多相似的代码行。
在大多数情况下,最好将它们改为辅助函数,并让编译器优化。但有时你会遇到无法这样做的情况。
- 你需要为每种功能配置生成不同的类型(例如,不仅仅是逻辑或 impl)。
- 你使用它来为宏正在实现的函数提供函数体,并且你无法将此函数体泛化到任何 trait 约束上。
- 当你的宏影响周围的控制流时(break、continue、return)。
- 出于优化原因(你肯定已经做过基准测试)。
改变控制流时要小心。
对于非常简单的局部辅助宏,它可能有一些用处。但我倾向于认为导出此类宏是一种反模式。然而,在独特情况下非常节制地使用,以协助实现某些东西时,它可能会有帮助。
老实说,即使是对于 crate 本地宏,我也倾向于避免这种做法。根据我的经验,混淆控制流会对可读性产生负面影响。
这种模式出于优化原因被使用的一个例子是在 Rust 标准库中。str 类型出于优化原因根据字符串长度进行特化(specialize_for_lengths;在此处使用)。
bytes crate 实际上使用了一些宏来减少代码重复。参见 crate 本地的 buf_get_impl 宏。这是宏为一组函数提供通用实现的好例子。正在实现的函数只需调用宏来展开这个通用实现。
在 bitflags crate 中可以找到用于生成新类型的 crate 导出宏的示例。在这个例子中,它使用了一些看起来有点像常规 Rust 代码的自定义语法,因此它是如何使用一些不同片段说明符(如 meta 或 vis)的良好参考。
最后,可能最流行的影响控制流的宏之一是 anyhow 的 bail! 宏。如前所述,我不喜欢这类宏(包括这个)。但我还是想提一下,因为它符合这组示例用例,并且可能有些受欢迎。(另一个潜在例子是标准库中用于轮询 future 的 ready! 宏。)
一个非常简单的例子是构造一个重定向到底层实现的 newtype。我们可能希望确保类型彼此实际不同,但可能不想重新实现该类型提供的功能(impl_displayable_type——此处有使用示例——是我自己的 crate typed-ident 中的一个例子)。
// A simplified form of impl_displayable_type to use as an example. macro_rules! impl_displayable_type { ( name=$name:ident, over=$over:ident, docs=$docs:expr, ) => { #[doc = $docs] pub struct $name($over);
impl core::fmt::Display for $name { #[inline] fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { self.0.fmt(f) } } }; }
// We have some displayable types that we don't want to conflate.
impl_displayable_type! { name = ObjectId, over = u64, docs = "Example documentation here", }
impl_displayable_type! { name = GroupId, over = u64, docs = concat!( "docs 是一个表达式,所以你可以做这样的事情。\n", "\n", "多行文档,为了更好的可读性!", ), }
// 那么现在,如果我们有这样一个 API…… struct Object; impl Object { pub fn object_id(&self) -> ObjectId { todo!() } pub fn group_id(&self) -> GroupId { todo!() } }
// ……你就不会意外地做出这样的事情。 fn example_bad_usage_that_no_longer_compiles(object: Object) { let group_id: GroupId = object.object_id(); // 编译失败! }
在更适合使用 #[derive(...)] 过程宏的地方,人们很容易想去用声明式宏。除非必要,尽量避免创建这类宏。声明式宏的一个常见玩具示例是在枚举上创建一个函数,以便获取枚举变体名称的静态字符串。但这可以通过派生宏来实现,而且在我看来那样更符合习惯(比如 strum crate 中的 IntoStaticStr,或者——也许更好——用一个自定义 trait 来表示)。做你认为最终用户会觉得最自然的事。如果你的宏所做的事是实现一个 trait——你可能就应该把它做成派生宏,除非你有充分的理由不这样做。不过,我们会在后面讲过程宏的文章里更多地讨论派生宏。
在更适合使用 #[derive(...)] 过程宏的地方,人们很容易想去用声明式宏。除非必要,尽量避免创建这类宏。
声明式宏的一个常见玩具示例是在枚举上创建一个函数,以便获取枚举变体名称的静态字符串。但这可以通过派生宏来实现,而且在我看来那样更符合习惯(比如 strum crate 中的 IntoStaticStr,或者——也许更好——用一个自定义 trait 来表示)。
做你认为最终用户会觉得最自然的事。如果你的宏所做的事是实现一个 trait——你可能就应该把它做成派生宏,除非你有充分的理由不这样做。
不过,我们会在后面讲过程宏的文章里更多地讨论派生宏。
总结
感谢阅读!
宏是一个非常复杂的主题,所以感谢你陪我读完这些文章,看我努力解释它们。
如果你能学到新东西,并且想表达你的感谢,请考虑在 Ko-Fi 上捐赠!
我很希望能全职与社区合作,这样的支持能让我继续写这样的文章。
在本文中,我们:
- 了解了宏的作用域,以及如何导出声明式宏。
- 学习了一种组织声明式宏以便于维护的方法。
- 探索了一小部分声明式宏的用例。
- 看了一些声明式宏用法的示例(一些是真实世界的,另一些是玩具示例)。
关于声明式宏,我还有一点想说的……
本文的初稿有 7500 词,太长了。下一篇文章将聚焦于声明式宏的最佳实践和常见痛点。敬请期待!
我保证我们最终会讲到过程宏的。:)