14 个版本
0.3.2 | 2023年6月23日 |
---|---|
0.3.1 | 2023年6月2日 |
0.3.0 | 2023年5月19日 |
0.2.2 | 2023年5月12日 |
0.1.3 | 2023年2月25日 |
1021 在 Rust 模式
每月 69 次下载
57KB
591 行(不包括注释)
'trait-gen' 包
此库提供了一个属性宏,用于为几种类型生成特质实现,无需自定义声明性宏、代码重复或泛型实现。这使得代码更易于阅读和维护。
以下是一个简短的示例
use trait_gen::trait_gen;
#[trait_gen(T -> u8, u16, u32, u64, u128)]
impl MyLog for T {
fn my_log2(self) -> u32 {
T::BITS - 1 - self.leading_zeros()
}
}
trait_gen
属性通过替换 T
为作为参数提供的类型生成以下代码
impl MyLog for u8 {
fn my_log2(self) -> u32 {
u8::BITS - 1 - self.leading_zeros()
}
}
impl MyLog for u16 {
fn my_log2(self) -> u32 {
u16::BITS - 1 - self.leading_zeros()
}
}
// and so on for the remaining types
使用方法
该属性放置在伪泛型实现代码之前。首先给出 泛型参数,然后是一个右箭头 (->
) 和一个类型参数列表。
#[trait_gen(T -> Type1, Type2, Type3)]
impl Trait for T {
// ...
}
属性宏依次将代码中的泛型参数 T
替换为以下类型 (Type1
、Type2
、Type3
) 以生成所有实现。
代码中所有以 T
开头的 类型路径 都被这部分替换。例如,T::default()
生成 Type1::default()
、Type2::default()
等等,但 super::T
保持不变,因为它属于另一个作用域。
代码必须与所有类型兼容,否则编译器将触发相关错误。例如,以下代码不能应用于 let x: T = 0;
,因为 0
不是一个有效的浮点字面量。
最后,实际类型将替换文档注释、宏和字符串字面量中出现的任何 ${T}
出现。
注意
- 使用字母 "T" 不是强制性的;任何类型路径都可以。例如,
gen::Type
也是可以的。但为了便于阅读并类似于泛型实现,建议使用简短的 uppercase 标识符。 - 可以将两个或多个属性链起来生成所有组合。
- 在类型实现上也可以使用
trait_gen
。
动机
有几种方法可以生成多个实现
- 手动复制
- 使用声明性宏
- 使用泛型实现
上面的实现示例可以通过这个 声明性宏 实现
macro_rules! impl_my_log {
($($t:ty)*) => (
$(impl MyLog for $t {
fn my_log2(self) -> u32 {
$t::BITS - 1 - self.leading_zeros()
}
})*
)
}
impl_my_log! { u8 u16 u32 u64 u128 }
但这很嘈杂,比原生代码难读。我们必须每次都编写一个自定义宏,包括它的声明、模式以及一些元素(例如参数)的转换(在这里,$t
)。此外,IDE 常常无法提供上下文帮助或在宏代码中应用重构。
当我们寻找方法定义时,如果它已经被声明性宏生成,那么看到这个结果会感到非常烦恼且无助于解决问题。
impl_my_log! { u8 u16 u32 u64 u128 }
使用 泛型实现 也有其他缺点
- 它禁止除同一crate中尚未被泛型实现覆盖的类型以外的任何实现,因此它只能在可以为所有绑定类型(当前和未来)编写实现时才有效。
- 找到与我们需要编写的功能相对应的特质并不总是可能的。例如,
num
crate 为原始类型提供了大量帮助,但并非所有内容都有涵盖。 - 即使操作和常量由特质覆盖,也会迅速需要很长的一串特质限制。
将第一个示例作为泛型实现编写如下。由于这是一个简短的示例,因此只有一个限制,但我们不得不使用一个不太好看的技巧来代替 T::BITS
。
use std::mem;
use num_traits::PrimInt;
impl<T: PrimInt> MyLog for T {
fn my_log2(self) -> u32 {
mem::size_of::<T>() as u32 * 8 - 1 - self.leading_zeros()
}
}
示例
以下是一些受支持的替换示例;你可以在库的 集成测试 中找到更多。
第一个示例更多地是为了说明什么可以替换,什么不可以替换,而不是一个实用的实现
#[trait_gen(U -> u32, i32, u64, i64)]
impl AddMod for U {
fn add_mod(self, other: U, m: U) -> U {
const U: U = 0;
let zero = U::default();
let offset: super::U = super::U(0);
(self + other + U + zero + offset.0 as U) % m
}
}
展开为(我们只显示第一个类型,u32
)
-
impl AddMod for u32 { fn add_mod(self, other: u32, m: u32) -> u32 { const U: u32 = 0; let zero = u32::default(); let offset: super::U = super::U(0); (self + other + U + zero + offset.0 as u32) % m } } // ...
这个示例展示了泛型特质中类型参数的使用
struct Meter<U>(U);
struct Foot<U>(U);
trait GetLength<T> {
fn length(&self) -> T;
}
#[trait_gen(U -> f32, f64)]
impl GetLength<U> for Meter<U> {
fn length(&self) -> U {
self.0 as U
}
}
此属性可以与另一个属性结合使用,创建一个通用组合,实现Meter<f32>
、Meter<f64>
、Foot<f32>
、Foot<f64>
等特质的实现。
#[trait_gen(T -> Meter, Foot)]
#[trait_gen(U -> f32, f64)]
impl GetLength<U> for T<U> {
fn length(&self) -> U {
self.0 as U
}
}
它被扩展为以下形式
-
impl GetLength<f32> for Meter<f32> { fn length(&self) -> f32 { self.0 as f32 } } impl GetLength<f64> for Meter<f64> { fn length(&self) -> f64 { self.0 as f64 } } impl GetLength<f32> for Foot<f32> { fn length(&self) -> f32 { self.0 as f32 } } impl GetLength<f64> for Foot<f64> { fn length(&self) -> f64 { self.0 as f64 } }
多段路径(带有::
的路径)和路径参数(<f32>
)可以在参数中使用。例如,gen::U
用于避免与已经定义的许多单字母类型产生混淆。
此外,Meter
和Foot
在参数中必须保留units
模块路径,因为这些路径如果在代码中则不会发生替换(在impl Add for units::gen::U
中的类型不以前缀gen::U
开始,因此不会被替换)。
注意:gen
不需要任何声明,因为它会被宏替换。
#[trait_gen(gen::U -> units::Meter<f32>, units::Foot<f32>)]
impl Add for gen::U {
type Output = gen::U;
fn add(self, rhs: Self) -> Self::Output {
gen::U(self.0 + rhs.0)
}
}
可以使用更复杂的数据类型,例如引用或切片。以下示例生成了不可变、可变和boxed引用类型的实现。
#[trait_gen(T -> u8, u16, u32, u64, u128)]
impl MyLog for T {
fn my_log2(self) -> u32 {
T::BITS - 1 - self.leading_zeros()
}
}
#[trait_gen(T -> u8, u16, u32, u64, u128)]
#[trait_gen(U -> &T, &mut T, Box<T>)]
impl MyLog for U {
fn my_log2(self) -> u32 {
MyLog::my_log2(*self)
}
}
如您在通用组合中看到的那样,第一个泛型参数U
可以在第二个属性参数列表中使用(属性的顺序无关紧要)。
最后,这个示例展示了如何通过使用${T}
格式来在每个实现中自定义文档和字符串字面量。
trait Repr {
fn text(&self) -> String;
}
#[trait_gen(T -> u32, i32, u64, i64)]
impl Repr for T {
/// Produces a string representation for `${T}`
fn text(&self) -> String {
call("${T}");
format!("${T}: {}", self)
}
}
assert_eq!(1_u32.text(), "u32: 1");
assert_eq!(2_u64.text(), "u64: 2");
-
impl Repr for u32 { /// Produces a string representation for `u32` fn text(&self) -> String { call("u32"); format!("u32: {}", self) } } // ...
注意:没有转义代码来避免替换;如果您需要${T}
用于其他目的,并且不希望它被替换,您必须选择另一个泛型参数;例如,U
或my::T
。
旧格式
早期版本中使用的属性使用了更短的格式,尽管这可能更难以阅读,但现在仍然得到支持。
#[trait_gen(Type1, Type2, Type3)]
impl Trait for Type1 {
// ...
}
在这里,代码以原样为Type1
生成,然后Type2
和Type3
被替换为Type1
以生成它们的实现。这是另一种格式的等效属性的快捷方式。
#[trait_gen(Type1 -> Type1, Type2, Type3)]
impl Trait for Type1 {
// ...
}
当没有冲突风险时,例如以下示例中所示,可以使用旧格式。所有Meter
类型都必须更改,并且不太可能与Foot
和Mile
混淆。要替换的代码中的类型必须在参数列表中第一个。
use std::ops::Add;
use trait_gen::trait_gen;
pub struct Meter(f64);
pub struct Foot(f64);
pub struct Mile(f64);
#[trait_gen(Meter, Foot, Mile)]
impl Add for Meter {
type Output = Meter;
fn add(self, rhs: Meter) -> Self::Output {
Self(self.0 + rhs.0)
}
}
小心不要替换必须在所有实现中保持相同的类型!考虑以下示例,其中返回类型始终是u64
。
pub trait ToU64 {
fn into_u64(self) -> u64; // always returns a u64
}
#[trait_gen(u64, i64, u32, i32, u16, i16, u8, i8)]
impl ToU64 for u64 {
fn into_u64(self) -> u64 { // ERROR! Replaced by i64, u32, ...
self as u64
}
}
此代码无法工作,因为u64
恰好也是列表中的第一个类型。使用不同的第一个类型,如i64
,或者使用非旧格式。
替代格式
当启用 in_format
功能时,也支持另一种格式。
trait-gen = { version="0.3", features=["in_format"] }
警告:此功能是临时的,不能保证其维护。
在这里,使用 in
代替箭头 ->
,并且参数类型必须放在方括号内。
use trait_gen::trait_gen;
#[trait_gen(T in [u8, u16, u32, u64, u128])]
impl MyLog for T {
fn my_log2(self) -> u32 {
T::BITS - 1 - self.leading_zeros()
}
}
使用此格式会发出 '已弃用' 警告,您可以通过在文件顶部添加 #![allow(deprecated)]
指令或添加 #[allow(deprecated)]
来关闭这些警告。
IDE 代码意识
rust-analyzer 支持代码感知的过程宏,因此对于基于此语言服务器协议实现的编辑器来说,一切应该都很正常。可能需要 rustc 的夜间版本,但不是默认版本。
对于 IntelliJ 插件,这是一个正在进行的工作,可以通过 此问题 跟踪。目前,在插件版本 0.4.190.5263-223 下,IDE 已经正确考虑了替换,用户可以在弹出窗口中查看展开的代码。但这是实验性的,并且必须手动 激活此功能。
请注意,属性过程宏展开默认是禁用的。如果您想尝试,请启用
org.rust.macros.proc.attr
实验性功能。调用帮助 | 查找操作(或按 Ctrl+Shift+A)并搜索“实验性功能”。在实验性功能对话框中,开始输入功能的名称或在列表中查找它,然后选择或清除复选框。
作为替代方案,如果您不想激活此功能,可以定义别名。例如,type T = Type1;
,并为 T
编写实现代码。IDE 将提供对 Type1
的预期帮助,但不会对其他参数类型提供帮助。或者您可以使用旧格式。
限制
-
trait_gen
属性的过程宏无法处理作用域,因此它不支持任何与泛型参数相同的字面量类型声明。例如,由于泛型函数,此代码无法编译。use num::Num; use trait_gen::trait_gen; trait AddMod { type Output; fn add_mod(self, rhs: Self, modulo: Self) -> Self::Output; } #[trait_gen(T -> u64, i64, u32, i32)] impl AddMod for T { type Output = T; fn add_mod(self, rhs: Self, modulo: Self) -> Self::Output { fn int_mod<T: Num> (a: T, m: T) -> T { // <== ERROR, conflicting 'T' a % m } int_mod(self + rhs, modulo) } }
-
泛型参数必须是一个 类型路径;它不能是更复杂的类型,如引用或切片。因此,您可以使用
gen::T<U> -> ...
,但不能使用&T -> ...
。
兼容性
trait-gen
包已在 Windows 64 位和 Linux 64/32 位平台上针对 rustc 1.58.0 及更高版本进行了测试。
版本
RELEASES.md 记录了所有版本的日志。
许可证
此代码可根据您的选择,在以下许可证下使用:MIT 许可证 或 Apache 许可证 2.0。
依赖项
约1.5MB
约36K SLoC