如何将“元数据”分配给特征?
我有2个宏。第一个是special_trait,一个用于特征声明的属性宏,第二个useful_macro是与这样的特征一起使用。
也就是说,用户代码会这样写:
#[special_trait]
pub trait MyTrait{}
// meanwhile, in a different file...
use some_mod::MyTrait;
useful_macro!(MyTrait);
现在,special_trait宏需要以可以使用它MyTrait的方式分配一些元数据useful_macro。
这是可能的吗?
可能但次优的解决方案
我突然想到我可以要求所有用户代码指定特征的完整路径,而不是依赖于use:
#[special_trait]
pub trait MyTrait{}
// meanwhile, in a different file...
useful_macro!(some_mod::MyTrait);
然后,special_trait只需要定义一个pub const MyTrait_METADATA: i32 = 42, 并且useful_macro可以找到这个元数据 const 的some_mod::MyTrait路径,因为它有完整的路径,只需要更改最后一段:some_mod::MyTrait_METADATA。
但是,禁止use和要求完整路径似乎很卑鄙,如果有更好的方法,我不想这样做。
我是否可以将元数据常量与特征相关联,以便任何可以“访问”特征的宏也可以找到元数据?
回答
Rocket v4 也有同样的问题:
当在根模块之外的模块中声明路由时,您可能会发现在挂载时遇到意外错误:
mod other { #[get("/world")] pub fn world() -> &'static str { "Hello, world!" } } #[get("/hello")] pub fn hello() -> &'static str { "Hello, outside world!" } use other::world; fn main() { // error[E0425]: cannot find value `static_rocket_route_info_for_world` > in this scope rocket::ignite().mount("/hello", routes![hello, world]); }出现这种情况是因为路由!宏将路由名称隐式转换为 Rocket 代码生成的结构名称。解决方案是使用命名空间路径来引用路由:
rocket::ignite().mount("/hello", routes![hello, other::world]);
在 Rocket v5(目前,只有一个候选版本)中,这种情况不再发生。例如,这用 Rocket v5 编译:
#[macro_use]
extern crate rocket;
mod module {
#[get("/bar")]
pub fn route() -> &'static str {
"Hello, world!"
}
}
use module::route;
fn main() {
rocket::build().mount("/foo", routes![route]);
}
运行cargo-expand时,我们看到 Rocket 生成了这样的东西(我缩写):
#[macro_use]
extern crate rocket;
mod module {
pub fn route() -> &'static str {
"Hello, world!"
}
#[doc(hidden)]
#[allow(non_camel_case_types)]
/// Rocket code generated proxy structure.
pub struct route {}
/// Rocket code generated proxy static conversion implementations.
impl route {
#[allow(non_snake_case, unreachable_patterns, unreachable_code)]
fn into_info(self) -> ::rocket::route::StaticInfo {
// ...
}
// ...
}
// ...
}
// ...
get应用于函数的属性宏构造了一个与函数同名的新结构。该结构体包含元数据(或者,更准确地说,包含一个函数into_info(),该函数返回一个具有正确元数据的结构体——尽管这更多的是 Rocket 使用的实现的细节)。
这是有效的,因为函数声明位于 Value Namespace 中,而 struct 声明位于 Type Namespace 中。该use声明同时导入.
让我们将其应用于您的示例:您的特征声明位于类型命名空间中,就像结构一样。因此,虽然您不能让special_trait宏声明一个与 trait 同名的结构,但您可以让该宏声明一个同名的函数,该函数返回一个包含元数据的结构。然后可以调用此函数useful_macro!来访问特征的元数据。因此,例如,元数据结构可能如下所示:
struct TraitMetadata {
name: String
}
然后您的宏可以扩展如下:
mod other {
#[special_trait]
pub trait MyTrait{}
}
use some_mod::MyTrait;
fn main() {
useful_macro!(MyTrait);
}
对此:
mod other {
pub trait MyTrait{}
pub fn MyTrait() -> TraitMetadata {
TraitMetadata {
name: "MyTrait".to_string()
}
}
}
use other::MyTrait;
fn main() {
do_something_with_trait_metadata(MyTrait());
}
这种设计只有一个问题:如果用户声明了一个与 trait 同名的函数(或存在于值命名空间中的任何其他东西),这将失败。然而:
- 在惯用的 Rust 中,函数名称是
snake_case而特征名称是CamelCase,因此如果用户使用惯用标识符,他将永远不会拥有与特征使用的名称相同的函数。 - 即使用户使用非惯用名称,对特征和函数使用相同的标识符也只是自找麻烦。我怀疑任何人(好吧,除了宏作者之外的任何人)都会这样做。
因此,这可能导致冲突的唯一现实方法是另一个宏作者也使用此构造将元数据附加到特征,并且用户将您的属性宏和另一个宏应用于同一 trait。在我看来,这是一个边缘情况,很少发生,不值得支持。