我可以在两个特质之间转换吗?

问题描述 投票:3回答:4

是否有办法将一个特征转换为另一个特征?

我具有特征FooBarVec<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)
rust traits
4个回答
7
投票

没有无法在两个不相关的特征之间进行转换。要了解原因,我们必须了解特征对象是如何实现的。首先,让我们看一下TraitObject

TraitObject反映了特质对象的实际实现方式。它们由两个指针组成:TraitObjectdatavtable值只是对原始对象的引用:

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_fooBar的枚举,然后进行模式匹配。

  • 对于该实现,您可以将Box<Foo>的主体移到Box<Bar>的主体中。

  • 您可以实现第三个特征bar,其中调用foo会调用Quux,先调用<FooStruct as Quux>::quux会先调用Foo::foo,然后再调用<BarStruct as Quux>::quux


2
投票

所以...我不认为这正是您想要的,但这是我能得到的最接近的。

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,因此我们并没有真正将一个特征转换为另一个特征)


2
投票

简短的答案是:目前对这种语言的向下转换的支持非常有限。


长答案是,由于技术和哲学原因,都不能将垂头丧气视为高度优先事项:

  • 从技术角度来看,对于大多数(如果不是全部)情况,都有变通办法
  • 从哲学的角度来看,向下转换导致软件更加脆弱(因为您意外地开始依赖于实现细节)

[有多个提议,我本人Foo,但目前还没有被选中,目前尚不清楚Rust是否会垂头丧气,或者它是否会发挥其局限性。

同时,您基本上有两种解决方法:

  1. 使用Box<Foo>:每种类型都有一个相关的participated值,可以查询该值,然后您可以构建一个类型擦除的容器,例如TypeId,并查询其持有的类型是否为特定的X。场景TypeId只需将X的Any与所存储值的Any进行比较即可。

  2. 像您一样创建一个特定的Any

后者更开放,尤其可以与特征一起使用,而前者仅限于具体类型。


2
投票

在读完所有这些很棒的答案之后,这就是我所做的。

我向TypeId特性添加了TypeId方法,该方法返回trait。我为trait提供了一个默认实现以返回as_bar,以便对Foo实现者的不便之处几乎没有,也没有麻烦。

Option<&Bar>

仅适用于同时实现NoneFooBar结构,我需要覆盖该方法以返回trait Foo { fn do_foo (&self); fn as_bar(&self) -> Option<&Bar> { None } }

SomeFooBar

这使调用代码看起来很像我想要的样子。

Foo

游戏围栏:Bar

我很乐意看到锈在将来得到改善,但这是我可以完全接受的解决方案。

© www.soinside.com 2019 - 2024. All rights reserved.