是否有办法将一个特征转换为另一个特征?
我具有特征Foo
和Bar
和Vec<Box<dyn Foo>>
。我知道Vec
中的某些项目实现了Bar
特性,但是有什么办法可以将它们定位为目标?
我不知道这是否可行。
trait Foo {
fn do_foo(&self);
}
trait Bar {
fn do_bar(&self);
}
struct SomeFoo;
impl Foo for SomeFoo {
fn do_foo(&self) {
println!("doing foo");
}
}
struct SomeFooBar;
impl Foo for SomeFooBar {
fn do_foo(&self) {
println!("doing foo");
}
}
impl Bar for SomeFooBar {
fn do_bar(&self) {
println!("doing bar");
}
}
fn main() {
let foos: Vec<Box<dyn Foo>> = vec![Box::new(SomeFoo), Box::new(SomeFooBar)];
for foo in foos {
foo.do_foo();
// if let Some(val) = foo.downcast_whatever::<Bar>() {
// val.bar();
// }
}
}
[Playground](https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=8b637bddc4fc923ce705e84ad1d783d4)
没有无法在两个不相关的特征之间进行转换。要了解原因,我们必须了解特征对象是如何实现的。首先,让我们看一下TraitObject
。
TraitObject
反映了特质对象的实际实现方式。它们由两个指针组成:TraitObject
和data
。 vtable
值只是对原始对象的引用:
data
#![feature(raw)]
trait Foo {}
impl Foo for u8 {}
use std::{raw, mem};
fn main() {
let i = 42u8;
let t = &i as &Foo;
let to: raw::TraitObject = unsafe { mem::transmute(t) };
println!("{:p}", to.data);
println!("{:p}", &i);
}
指向函数指针表。该表包含对每个已实现的trait方法的引用,这些引用按某种编译器内部的方式排序。
vtable
因此,特征对象知道指向数据的指针和构成该特征的方法列表的指针。从此信息中,无办法可以获取任何其他数据。
嗯,几乎没办法。您可能会猜到,您可以在vtable中添加一个返回different特征对象的方法。在计算机科学中,所有问题都可以通过添加另一层间接寻址来解决(太多层的间接寻址除外)。
但是不能将
// For this hypothetical input trait Foo { fn one(&self); } impl Foo for u8 { fn one(&self) { println!("u8!") } } // The table is something like this pseudocode const FOO_U8_VTABLE = [impl_of_foo_u8_one];
的data
部分转换为结构
不安全,不。特征对象不包含有关原始类型的信息。它仅有的一个原始指针包含内存中的地址。您可以unsafely将其转换为TraitObject
或&Foo
或&u8
,但是编译器和运行时数据都不知道它最初是什么具体类型。
&()
实际上是通过跟踪原始结构的类型ID来实现的。如果您要求引用正确的类型,则特征将为您转换数据指针。
除了我用我的
Any
trait特征描述的模式之外,是否有其他模式可以处理这样的情况,即我们需要遍历一堆特征对象,但要对其中一些对象稍作不同?
如果拥有这些特征,则可以将Any
添加到FooOrBar
特征中,反之亦然。
您可以创建一个包含as_foo
或Bar
的枚举,然后进行模式匹配。
对于该实现,您可以将Box<Foo>
的主体移到Box<Bar>
的主体中。
您可以实现第三个特征bar
,其中调用foo
会调用Quux
,先调用<FooStruct as Quux>::quux
会先调用Foo::foo
,然后再调用<BarStruct as Quux>::quux
。
所以...我不认为这正是您想要的,但这是我能得到的最接近的。
Bar::foo
注意,只能将Bar::bar
称为// first indirection: trait objects
let sf: Box<Foo> = Box::new(SomeFoo);
let sb: Box<Bar> = Box::new(SomeFooBar);
// second level of indirection: Box<Any> (Any in this case
// is the first Box with the trait object, so we have a Box<Box<Foo>>
let foos: Vec<Box<Any>> = vec![Box::new(sf), Box::new(sb)];
// downcasting to the trait objects
for foo in foos {
match foo.downcast::<Box<Foo>>() {
Ok(f) => f.do_foo(),
Err(other) => {
if let Ok(bar) = other.downcast::<Box<Bar>>() {
bar.do_bar();
}
}
}
}
,因为我们首先将其存储为SomeFooBar
。因此,这仍然不是您想要的(Box<Bar>
也是Box<Bar>
,但是您无法再将其转换为SomeFooBar
,因此我们并没有真正将一个特征转换为另一个特征)
简短的答案是:目前对这种语言的向下转换的支持非常有限。
长答案是,由于技术和哲学原因,都不能将垂头丧气视为高度优先事项:
[有多个提议,我本人Foo
,但目前还没有被选中,目前尚不清楚Rust是否会垂头丧气,或者它是否会发挥其局限性。
同时,您基本上有两种解决方法:
使用Box<Foo>
:每种类型都有一个相关的participated值,可以查询该值,然后您可以构建一个类型擦除的容器,例如TypeId
,并查询其持有的类型是否为特定的X。场景TypeId
只需将X的Any
与所存储值的Any
进行比较即可。
像您一样创建一个特定的Any
。
后者更开放,尤其可以与特征一起使用,而前者仅限于具体类型。
在读完所有这些很棒的答案之后,这就是我所做的。
我向TypeId
特性添加了TypeId
方法,该方法返回trait
。我为trait提供了一个默认实现以返回as_bar
,以便对Foo
实现者的不便之处几乎没有,也没有麻烦。
Option<&Bar>
仅适用于同时实现
None
和Foo
的Bar
结构,我需要覆盖该方法以返回trait Foo { fn do_foo (&self); fn as_bar(&self) -> Option<&Bar> { None } }
。
SomeFooBar
这使调用代码看起来很像我想要的样子。
Foo
游戏围栏:
Bar
我很乐意看到锈在将来得到改善,但这是我可以完全接受的解决方案。