6 个版本

0.3.0 2023年7月27日
0.3.0-alpha.12023年7月25日
0.2.3 2023年6月11日
0.2.1 2023年2月11日
0.1.0 2023年2月5日

#660Rust 模式

Download history 12/week @ 2024-04-20 33/week @ 2024-04-27 431/week @ 2024-05-04 35/week @ 2024-05-11 98/week @ 2024-05-18 95/week @ 2024-05-25 81/week @ 2024-06-01 113/week @ 2024-06-08 36/week @ 2024-06-15 80/week @ 2024-06-22 28/week @ 2024-06-29 154/week @ 2024-07-06 172/week @ 2024-07-13 151/week @ 2024-07-20 173/week @ 2024-07-27 70/week @ 2024-08-03

每月 569 次下载
7 个 crate 中使用 (直接使用 2 个)

Apache-2.0

32KB
161

portrait

GitHub actions crates.io crates.io docs.rs GitHub GitHub

用默认值、委托等填充 impl-trait 块。

动机

Rust 的 trait 支持提供的方法,这对于向后兼容性和实现编码效率非常有用。然而,它们也有一些限制

  • 如果关联函数的返回类型是关联类型,则没有合理的方法来实现它。
  • 如果 trait 包含许多高度相似的相关函数,则编写默认值将涉及大量的样板代码。但用户只能通过过程宏为每个方法提供一个默认实现。

使用 portrait,默认实现是在 impl 级别粒度而不是 trait 级别提供的。

使用方法

首先,使用 #[portrait::make] 属性创建要实现的 trait 的肖像

#[portrait::make]
trait FooBar {
  // ...
}

部分实现 trait 并将剩余部分留给 #[portrait::fill] 属性

#[portrait::fill(portrait::default)]
impl FooBar {}

portrait::default 部分是 "填充宏" 的路径,它实际上是填充 impl 块的项目。语法类似于 #[derive(path::to::Macro)]

如果有不同模块中的实现,则需要在 make 属性中手动传递 trait 项中使用的导入

#[portrait::make(import(
  foo, bar::*,
  // same syntax as use statements
))]
trait FooBar {...}

如果填充属性因未定义的错误而失败,例如关于 foo_bar_portrait 的错误,请手动导入它以及 FooBar 特性;#[portrait::make] 属性在同一个模块中生成了 FooBar 特性模块内的这个新模块。

提供的填充项

portrait 提供以下填充宏

  • default:通过委托到 Default::default() 实现每个缺失的方法和常量(Default 是 const-unstable 并需要 nightly 版本,带有 #![feature(const_default_impls)])。
  • delegate:将每个缺失的方法、常量和类型代理到一个表达式(通常是 self.field)或实现相同特性的其他类型。
  • lo:调用一个类似 format! 的宏,使用方法参数。

这是如何工作的

Rust 宏在编译链的早期阶段被调用。因此,属性宏只能访问它们应用的项目的字面表示,并且无法直接进行跨项目派生。大多数宏通过尝试生成不受不可访问信息影响的有效代码来规避这个问题,例如,Default 派生宏通过调用 Default::default() 来工作,而不检查实际的字段类型是否实现了 Default(因为编译器会在稍后阶段进行)。

遗憾的是,这种方法在 portrait 的用例中不起作用,其中属性宏需要编译时(过程宏运行时)访问 impl 块中引用的特性的项目;唯一可用的信息是特性的路径(甚至可能被重命名为不同的标识符)。

portrait 通过要求特性提前以令牌流的形式导出其信息(其“肖像”)来应对这一挑战。通过 #[portrait::make] 属性,生成了一个具有相同标识符的 声明式 宏,其中包含特性项。然后,impl 块上的(#[portrait::fill])属性将其输入传递给声明式宏,后者再将它们转发给实际的属性实现(例如 #[portrait::make]),同时附带特性项。

现在实际属性可以访问特性和用户实现,但这还不是故事的结局。特性项是在特性定义的作用域中编写的,但属性宏的输出是在实现定义的作用域中。最明显的影响是,特性模块中的导入不会影响实现输出。为了避免因特性模块的变化而频繁更新实现者,#[portrait::make] 属性还派生出一个模块,其中包含在特性中使用的导入,这些导入会自动导入到实现块中。

结果发现,由于当前编译器的限制,即使导入的类型是公开的,私有导入实际上也不能公开重新导出,因此自动扫描特性项以找到重新导出的路径变得不切实际(预定义类型也需要特殊处理,因为它们不是模块的一部分)。因此,异构作用域的问题不可避免地暴露给用户:要么通过导入手动全部重新导出类型,要么使导入对更广的作用域可见。

另一个困难是,模块标识符与特性标识符共享相同的符号空间(因为 module::FooTrait::Foo 无法区分)。因此,包含导入的模块不能与特性一起导入,用户必须手动导入/导出这两个符号,除非通过其封装模块引用特性。

免责声明

portrait 并非第一个在属性中使用声明性宏的项目。macro_rules_attribute 也实现了类似的想法,尽管没有涉及生成 macro_rules! 部分的框架。

依赖关系

~1–1.5MB
~32K SLoC