#traits #cast #any #upcast

no-std cast_trait_object

仅使用安全 Rust 在特例对象之间进行转换

4 个版本

0.1.3 2021 年 1 月 16 日
0.1.2 2020 年 11 月 4 日
0.1.1 2020 年 9 月 10 日
0.1.0 2020 年 9 月 10 日

#1119Rust 模式

Download history 19/week @ 2024-03-11 34/week @ 2024-03-18 28/week @ 2024-03-25 45/week @ 2024-04-01 22/week @ 2024-04-08 25/week @ 2024-04-15 24/week @ 2024-04-22 19/week @ 2024-04-29 36/week @ 2024-05-06 22/week @ 2024-05-13 16/week @ 2024-05-20 15/week @ 2024-05-27 27/week @ 2024-06-03 19/week @ 2024-06-10 20/week @ 2024-06-17 28/week @ 2024-06-24

每月 95 次下载
6 个包中使用 (通过 otter)

MIT/Apache

89KB
982

docs.rs crates.io

cast_trait_object

此包提供仅使用安全 Rust 和无平台特定代码在特例对象之间进行转换的功能。如果您想向下转换到具体类型而不是其他特例对象,则此包无法帮助您,而是使用 std::any 或类似 downcast-rs 的包。

此包提供两个功能,一个是抽象用于在特例对象之间进行转换的方法的特例 DynCast,以及一些宏以最小化实现此特例所需的样板代码。

用法

您应该在特例界限或作为超特例中使用 DynCast 特例,然后使用 DynCastExt 特例提供的方法进行转换。 DynCast 特例接受一个类型参数,该参数应由 create_dyn_cast_config 宏生成的 "config" 类型,此类型定义了从哪个特例到哪个特例进行转换。需要允许转换以满足 DynCast 特例界限的类型可以通过 impl_dyn_cast 宏实现它。

示例

use cast_trait_object::{create_dyn_cast_config, impl_dyn_cast, DynCast, DynCastExt};

create_dyn_cast_config!(SuperToSubCast = Super => Sub);
create_dyn_cast_config!(SuperUpcast = Super => Super);
trait Super: DynCast<SuperToSubCast> + DynCast<SuperUpcast> {}
trait Sub: Super {}

struct Foo;
impl Super for Foo {}
impl Sub for Foo {}
impl_dyn_cast!(Foo as Super => Sub, Super);

let foo: &dyn Super = &Foo;
// Casting to a sub trait is fallible (the error allows us to keep using the
// `dyn Super` trait object if we want which can be important if we are casting
// movable types like `Box<dyn Trait>`):
let foo: &dyn Sub = foo.dyn_cast().ok().unwrap();
// Upcasting to a supertrait is infallible:
let foo /*: &dyn Super*/ = foo.dyn_upcast::<dyn Super>();

通过 impl_dyn_cast 宏实现 DynCast 特例时,您还可以列出创建的 "config" 类型,而不是源特例和目标特例

impl_dyn_cast!(Foo => SuperToSubCast, SuperUpcast);

如果启用了 proc-macros 功能(默认情况下是启用的),我们还可以使用过程式属性宏来减少一些样板代码。

use cast_trait_object::{dyn_cast, dyn_upcast, DynCastExt};

#[dyn_cast(Sub)]
#[dyn_upcast]
trait Super {}
trait Sub: Super {}

struct Foo;
#[dyn_cast(Sub)]
#[dyn_upcast]
impl Super for Foo {}
impl Sub for Foo {}

请注意,#[dyn_upcast]#[dyn_cast(Super)] 做的是同一件事,但它更清晰地表达了意图。

use cast_trait_object::{dyn_cast, DynCastExt};

#[dyn_cast(Super, Sub)]
trait Super {}
trait Sub: Super {}

struct Foo;
#[dyn_cast(Super, Sub)]
impl Super for Foo {}
impl Sub for Foo {}

let foo: &dyn Sub = &Foo;
// Upcasting still works:
let foo /*: &dyn Super*/ = foo.dyn_upcast::<dyn Super>();

泛型

泛型特性和类型受到支持,并且声明性宏(impl_dyn_castcreate_dyn_cast_configimpl_dyn_cast_config)和过程式属性宏(dyn_castdyn_upcast)都可以与泛型一起使用。

use cast_trait_object::{DynCastExt, dyn_cast, dyn_upcast};

// Define a source and target trait:
#[dyn_cast(Sub<T>)]
#[dyn_upcast]
trait Super<T> {}
trait Sub<T>: Super<T> {}

// Since `T` isn't used for `Color` it doesn't need to be `'static`:
struct Color(u8, u8, u8);
#[dyn_cast(Sub<T>)]
#[dyn_upcast]
impl<T> Super<T> for Color {}
impl<T> Sub<T> for Color {}

struct Example<T>(T);
#[dyn_cast(Sub<T>)]
#[dyn_upcast]
impl<T: 'static> Super<T> for Example<T> {}
impl<T: 'static> Sub<T> for Example<T> {}

let as_sub: &dyn Sub<bool> = &Example(false);
let upcasted: &dyn Super<bool> = as_sub.dyn_upcast();
let _downcasted /*: &dyn Sub<bool> */ = upcasted.dyn_cast::<dyn Sub<bool>>().ok().unwrap();

请注意,当前对泛型类型的支持有一个限制,即如果实现 DynCast 的类型有任何泛型类型参数,则它们可能需要被约束为 'static

泛型类型还有一个限制,这可能有点反直观。由宏生成的 DynCast 实现必须始终成功或始终失败。这意味着,如果目标特性只为 DynCast 特性实现的类型子集实现,则转换将始终失败。

use cast_trait_object::{create_dyn_cast_config, impl_dyn_cast, DynCast, DynCastExt};

// Define a source and target trait:
create_dyn_cast_config!(UpcastConfig = Super => Super);
create_dyn_cast_config!(SuperConfig = Super => Sub);
trait Super: DynCast<SuperConfig> + DynCast<UpcastConfig> {}
trait Sub: Super {}

/// Only implements `Sub` for types that implement `Display`.
struct OnlyDisplayGeneric<T>(T);
impl<T: 'static> Super for OnlyDisplayGeneric<T> {}
impl<T: core::fmt::Display + 'static> Sub for OnlyDisplayGeneric<T> {}
// The cast to `Sub` will always fail since this impl of DynCast includes
// some `T` that don't implement `Display`:
impl_dyn_cast!(for<T> OnlyDisplayGeneric<T> as Super where {T: 'static} => Sub);
impl_dyn_cast!(for<T> OnlyDisplayGeneric<T> as Super where {T: 'static} => Super);

// &str does implement Display:
let _is_display: &dyn core::fmt::Display = &"";

// But the cast will still fail:
let as_super: &dyn Super = &OnlyDisplayGeneric("");
assert!(as_super.dyn_cast::<dyn Sub>().is_err());

// `OnlyDisplayGeneric<&str>` does implement `Sub`:
let as_sub: &dyn Sub = &OnlyDisplayGeneric("");

// Note that this means that we can perform an upcast and then fail to downcast:
let upcasted: &dyn Super = as_sub.dyn_upcast();
assert!(upcasted.dyn_cast::<dyn Sub>().is_err());

避免这种问题的最佳方法是使源特性实现和目标特性实现都具有相同的特性行界。

工作原理

转换是如何进行的

DynCast 特性作为超特性添加到特性对象v表中,这为特性对象添加了一些额外的方法。这些方法本质上都是接收一个类型指针,返回一个新的胖指针,该指针指向所需的v表。由于我们需要为每种类型的特性对象生成一个,因此需要为 &dyn Trait&mut dyn TraitBox<dyn Trait>Rc<dyn Trait>Arc<dyn Trait> 每种类型生成一个方法。请注意,这些方法完全是安全的Rust代码,这个crate不使用或生成任何不安全的代码。

这些方法的工作方式如下:

trait Super {}
trait Sub {
    fn upcast(self: Box<Self>) -> Box<dyn Super>;
}

impl Super for () {}
impl Sub for () {
    fn upcast(self: Box<Self>) -> Box<dyn Super> { self }
}

let a: Box<dyn Sub> = Box::new(());
let a: Box<dyn Super> = a.upcast();

DynCastExt 特性抽象了不同类型的特质对象,以便在调用 dyn_cast 方法时,编译器可以将该静态方法调用内联到特质对象上的正确方法。

为什么需要“config”类型

我们必须生成“config”类型,因为我们需要根据它所转换的特质唯一标识每个 DynCast 超特质。最初,这只是在特质的两个类型参数上完成的,类似于 DynCast<dyn Super, dyn Sub>,但当它们作为某个特质的超特质使用时,这会导致编译错误。因此,现在特质被“隐藏”在生成的“config”类型上的关联类型中。为了使这个“config”类型更易于使用,我们还实现了一个 GetDynCastConfig 特性,可以通过类似 <dyn Source as GetDynCastConfig<dyn Target>>::Config 的方式轻松地从源特质和目标特质到“config”类型。这使得宏(impl_dyn_castdyn_castdyn_upcast)可以接受特质作为参数,而不是“config”类型,这也使得 DynCastExt 特性的类型推断工作变得可行。

宏如何知道一个类型是否实现了“目标”特质

当一个类型实现了 DynCast 特质,并因此从源到目标特质的类型转换时,生成的代码必须选择转换是否成功。如果类型实现了 Target 特质,我们希望返回 Ok(value as &dyn Target),如果没有实现,则返回 Err(value as &dyn Source)

我们可以使用一个巧妙的技巧,仅在类型确实实现了目标特质时才执行强制转换。有关此技巧如何工作的更多信息,请参阅 dtolnay 的 [Autoref-based stable specialization](https://github.com/dtolnay/case-studies/tree/master/autoref-specialization)案例研究。简而言之,这个技巧允许我们在满足特质约束时调用一个方法,不满足时调用另一个方法。这样,我们可以在类型实际上实现了那个特质的情况下,调用一个执行类型转换到目标特质的辅助方法。

因此,我们可以生成类似

trait Source {}
trait Target {}

struct Foo;
impl Source for Foo {}

struct Fallback;
impl Fallback {
    fn cast<'a, T: Source>(&self, value: &'a T) -> &'a dyn Source { value }
}

struct HasTrait<T>(core::marker::PhantomData<T>);
impl<T> HasTrait<T> {
    fn new() -> Self {
         Self(core::marker::PhantomData)
    }
}
impl<T: Target> HasTrait<T> {
    fn cast<'a>(&self, value: &'a T) -> &'a dyn Target { value }
}
impl<T> core::ops::Deref for HasTrait<T> {
    type Target = Fallback;
    fn deref(&self) -> &Self::Target {
        static FALLBACK: Fallback = Fallback;
        &FALLBACK
    }
}

let used_fallback: &dyn Source = HasTrait::<Foo>::new().cast(&Foo);

所以,`impl_dyn_cast` 宏通过生成一个结构体来实现另一种类型的 core::ops::deref。这两个类型都有 cast 方法,但它们执行的操作不同。第一个结构的 cast 方法有一个特征约束,因此只有在转换可以成功时才会实现。如果第一个方法不可用,编译器将插入一个解引用操作(&*foo)并查看是否有方法可以应用在之后。在这种情况下,这意味着将调用 Fallback 结构体的方法。这样,生成的代码只有在实际实现 Target 特性时才调用执行转换到 Target 特性的方法。

替代方案

intertrait 包提供了与该包类似的功能,但实现方式完全不同,至少在 intertrait 版本 0.2.0 时是这样的。它使用 linkme 包来创建可以转换为特定特征对象的类型 std::any::Any 类型 ID 的注册表。这意味着当它使用 TypeId 在全局注册表中查找转换函数时,可能会有一些运行时开销。这也意味着它不能在所有平台上工作,因为 linkme 包需要为他们提供支持。这是该包没有的限制。

traitcast 包的工作方式与 intertrait 类似,它有一个以 TypeId 为键的全局注册表。但它与之不同的是,它使用 inventory 包来构建注册表,而不是使用 linkme 包。该 inventory 包使用 ctor 包在 main 之前运行一些代码,这通常是不被鼓励的,而且这正是 intertrait 将其作为其方法优势的原因。

traitcast_core 库允许使用不依赖于全局注册表的更底层 API,因此也不依赖于需要特定平台支持的包,如 linkmeinventory。相反,它要求你显式地创建一个注册表,并注册所有类型及其转换。

《downcast-rs》包提供了向下转型到具体类型的支持,但不能直接从一个特例对象转型到另一个特例对象。因此,它有不同的使用场景,并且在同一项目中,它和这个包都可能是有用的。

您可以在您的特例上定义方法,类似于这个包提供的DynCast特例。自己这样做可以更加灵活,例如,您可以通过只实现实际需要的转型方法来最小化膨胀。缺点是,这会比这个包提供的方法少很多便捷性。

参考

以下GitHub问题从VpnProvider超特例到子特例的伪向下转型清理,并采用更好的解决方案 · 问题 #21 · jamesmcm/vopono 启发了这个库。

这个库在以下博客文章的“向上转型”部分被提及:So you want to write object oriented Rust :: Darrien's Blog — Dev work and musings

许可证

该项目可以选择以下任意一种许可证发布:

由您选择。

贡献

除非您明确声明,否则,根据Apache-2.0许可证定义,您有意提交的任何贡献,包括但不限于在作品中包含的内容,都应按照上述方式双许可,不附加任何额外的条款或条件。

依赖

~0–295KB