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 日 |
在 #upcast 中排名 4
每月下载量 114
在 cast_trait_object 中使用
29KB
392 行
cast_trait_object
此 crate 提供了仅使用安全的 Rust 和无平台特定代码在 trait 对象之间进行转换的功能。如果您想将 downcast 到具体类型而不是其他 trait 对象,则此 crate 无能为力,而是可以使用类似 downcast-rs
的 crate。
用法
您应该在 trait 约束或作为超 trait 使用 DynCast
trait,然后使用 DynCastExt
trait 提供的方法进行转换。 DynCast
trait 采取一个类型参数,它应该是通过 create_dyn_cast_config
宏生成的 "config" 类型,此类型定义了转换是从哪个 trait 到哪个 trait。需要允许进行转换以满足 DynCast
trait 约束的类型可以通过 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
trait 时,您还可以列出创建的 "config" 类型而不是源和目标 trait
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 {}
// Upcasting still works:
let foo /*: &dyn Super*/ = foo.dyn_upcast::<dyn Super>();
工作原理
转换是如何进行的
使用DynCast
特质作为超特质,为特质对象的虚表添加了几个额外的方法。这些方法本质上都是接收一个类型指针并返回一个指向所需虚表的胖指针。由于我们需要为每种特质对象类型生成一个,所以为每个&dyn Trait
、&mut dyn Trait
、Box<dyn Trait>
、Rc<dyn Trait>
和Arc<dyn Trait>
生成一个方法。请注意,这些方法是完全安全的Rust代码,这个crate根本不使用或生成任何不安全的代码。
然后DynCastExt
特质将不同的特质对象类型抽象出来,这样当使用dyn_cast方法进行调用时,编译器可以将静态方法调用内联到特质对象的正确方法上。
为什么是“config”类型
我们必须生成“config”类型,因为我们需要根据它是从哪个特质转换到哪个特质来唯一地标识每个DynCast
超特质。最初,这只是在特质上使用两个类型参数来完成,类似于DynCast<dyn Super, dyn Sub>
,但是当它们被用作所提到的特质之一的超特质时,会导致编译错误。因此,现在特质被“隐藏”为生成“config”类型的关联类型。为了使这个“config”类型更易于使用,我们还实现了一个GetDynCastConfig
特质,可以通过类似<dyn Source as GetDynCastConfig<dyn Target>>::Config
的方式,轻松地从源特质和目标特质到“config”类型。这允许宏(impl_dyn_cast
、dyn_cast
和dyn_upcast
)接受特质作为参数而不是“config”类型,这也使得类型推断对DynCastExt
特质有效。
宏如何知道一个类型是否实现了“目标”特质
当一个类型实现了针对特定配置的DynCast
并因此从源特质到目标特质进行类型转换时,生成的代码必须选择转换是否成功。我们希望在类型实现了Target
特质时返回Ok(value as &dyn Target)
,如果它没有实现,则返回Err(value as &dyn Source)
。
我们可以使用一个巧妙的方法来确定类型是否实现了Target
特质。请参阅impls
存储库的GitHub页面了解这个方法是如何工作的。简而言之,这个方法允许我们获得一个const bool值,如果类型实现了特质则返回true
,否则返回false
。
因此,我们可以生成类似的内容:
trait Source {}
trait Target {}
struct Foo;
impl Source for Foo {}
const IMPLEMENTS_TRAIT: bool = false /* Really should use impls!(Foo: Target) */;
impl Foo {
fn cast(&self) -> Result<&dyn Target, &dyn Source> {
if IMPLEMENTS_TRAIT {
// Compile time error here:
Ok(self)
// ^^^^ the trait `Target` is not implemented for `Foo`
} else {
Err(self)
}
}
}
但是即使我们实际上永远不会运行将Foo
强制转换为Target
的代码,它也无法编译。因此,由于将类型强制转换为它没有实现的特质是类型错误,我们需要使用我们的const值以某种方式影响生成的代码中的类型。
这可以通过使用const值作为数组的长度来获取一个类型(注意,布尔值可以被转换为usize
)。一旦我们有了类型,我们就可以使用Rust强大的类型系统根据我们的初始值选择不同的方法
struct Choose<T>(T);
impl Choose<[(); 0]> {
fn foo(arg: usize) -> &'static str { "false" }
}
impl Choose<[(); 1]> {
fn foo(arg: String, arg2: bool) -> bool { true }
}
// These methods have the same name but are actually totally different methods:
let foo: &'static str = Choose::<[(); 0]>::foo(1);
let foo: bool = Choose::<[(); 1]>::foo("some text".to_string(), false);
或使用一个特质
trait AltChoose { type Result; }
struct A;
impl A {
fn foo(arg: usize) -> &'static str { "false" }
}
impl AltChoose for [(); 0] { type Result = A; }
struct B;
impl B {
fn foo(arg: String, arg2: bool) -> bool { true }
}
impl AltChoose for [(); 1] { type Result = B; }
// These methods have the same name but are actually totally different methods:
let foo: &'static str = <[(); 0] as AltChoose>::Result::foo(1);
let foo: bool = <[(); 1] as AltChoose>::Result::foo("some text".to_string(), false);
因此,impl_dyn_cast
宏通过生成一个表示类型是否实现了特质的const bool
,然后使用这个const值与上述某个技巧一起确定在实现DynCast
特质时调用哪个辅助方法。这样,生成的代码只有在类型确实实现了它时才会调用执行强制转换到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,不依赖于全局注册表,因此也不依赖于需要特定平台支持的库如linkme
或inventory
。相反,它要求你显式创建一个注册表,并将所有类型及其转换注册到其中。
downcast-rs
库提供了向具体类型的向下转换,但不直接从一种特质对象转换到另一种特质对象。因此,它有不同的用途,并且它和这个库都可以在同一项目中使用。
参考文献
以下GitHub问题使用更好的解决方案清理从VpnProvider超特质到子特质的伪向下转换 · Issue #21 · jamesmcm/vopono启发了这个库。
许可证
该项目可以在以下任一许可证下发布:
由您选择。
依赖项
~2MB
~45K SLoC