7个版本

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日

#15 in #delegation

Download history 23/week @ 2024-04-06 86/week @ 2024-04-13 15/week @ 2024-04-20 36/week @ 2024-04-27 435/week @ 2024-05-04 37/week @ 2024-05-11 100/week @ 2024-05-18 99/week @ 2024-05-25 86/week @ 2024-06-01 117/week @ 2024-06-08 40/week @ 2024-06-15 85/week @ 2024-06-22 29/week @ 2024-06-29 154/week @ 2024-07-06 175/week @ 2024-07-13 152/week @ 2024-07-20

531 每月下载量
8 个crate中使用 (通过 portrait)

Apache-2.0

80KB
2K SLoC

portrait

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

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

动机

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

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

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

用法

首先,使用#[portrait::make]属性为要实现的特质制作肖像

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

部分实现特质,并将其余部分留给#[portrait::fill]属性

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

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

如果不同模块中有实现,需要手动将特质项中使用的导入传递给make属性

#[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)或实现相同特性的另一个类型。
  • log:使用方法参数调用类似 format! 的宏。

这是如何工作的

Rust 宏在编译链的早期阶段被调用。因此,属性宏只能访问它们应用的项目的大致表示,并且不能直接进行跨项目继承。大多数宏通过尝试生成不依赖于不可访问信息的代码来避免这个问题,例如,Default derive 宏通过调用 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