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日

1021Rust 模式

Download history 6/week @ 2024-03-12 9/week @ 2024-04-02 109/week @ 2024-04-09

每月 69 次下载

MIT/Apache 协议

57KB
591 行(不包括注释)

crate documentation build status crate



'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 替换为以下类型 (Type1Type2Type3) 以生成所有实现。

代码中所有以 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用于避免与已经定义的许多单字母类型产生混淆。

此外,MeterFoot在参数中必须保留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}用于其他目的,并且不希望它被替换,您必须选择另一个泛型参数;例如,Umy::T

旧格式

早期版本中使用的属性使用了更短的格式,尽管这可能更难以阅读,但现在仍然得到支持。

#[trait_gen(Type1, Type2, Type3)]
impl Trait for Type1 {
    // ...
}

在这里,代码以原样为Type1生成,然后Type2Type3被替换为Type1以生成它们的实现。这是另一种格式的等效属性的快捷方式。

#[trait_gen(Type1 -> Type1, Type2, Type3)]
impl Trait for Type1 {
    // ...
}

当没有冲突风险时,例如以下示例中所示,可以使用旧格式。所有Meter类型都必须更改,并且不太可能与FootMile混淆。要替换的代码中的类型必须在参数列表中第一个。

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