我可以在两个 trait 之间进行类型转换吗?

11

有没有一种方法可以从一个 trait 转换为另一个 trait?

我拥有 traits FooBar,以及一个 Vec<Box<dyn Foo>>。我知道其中一些项目实现了 Bar trait,但是是否有任何方法可以针对它们?

我不明白这是否可能。

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)

可能是 https://dev59.com/sYLba4cB1Zd3GeqPdmCb 的重复问题。 - Paolo Falabella
我认为它是相关的,但不完全相同。另外,它已经1.5年了,所以有很高的可能性发生了一些变化 ;) - Christoph
我找到的最接近的东西是这个http://stackoverflow.com/questions/27073799/rust-vector-of-traits-cast-each-trait?rq=1但它以编译器错误结束(不幸的是没有链接),而且也有点过时。 - Christoph
1
@Christoph Shepmaster 给了你几个选项(Paolo 给了你一个大锤 - 要明智地使用),但如果没有一些关于这些特征的上下文,很难给出关于如何建模解决方案的具体答案。第一个,也是最常见的本能反应是枚举,但它是否适合你的问题以及如何适合,真的还不确定。 - Veedrac
5个回答

12
不行。没有办法在两个无关的特质之间进行转换。要理解原因,我们必须了解特质对象是如何实现的。首先,让我们看一下TraitObjectTraitObject 反映了特质对象的实际实现方式。它们由两个指针组成:datavtabledata 值只是对原始对象的引用:
#![feature(raw)]

use std::{mem, raw};

trait Foo {}
impl Foo for u8 {}

fn main() {
    let i = 42u8;
    let t = &i as &dyn Foo;
    let to: raw::TraitObject = unsafe { mem::transmute(t) };

    println!("{:p}", to.data);
    println!("{:p}", &i);
}

“vtable” 指向一个函数指针表。这个表按照编译器内部的方式排序,包含对每个实现特性方法的引用。
对于这个假设的输入:
trait Foo {
    fn one(&self);
}

impl Foo for u8 {
    fn one(&self) { println!("u8!") }
}

这个表格类似于以下伪代码

const FOO_U8_VTABLE: _ = [impl_of_foo_u8_one];

一个特质对象知道指向数据的指针和组成该特质的方法列表的指针。从这些信息中,就无法获取任何其他数据。

嗯,几乎没有办法。正如你可能猜到的那样,您可以将返回不同特质对象的方法添加到虚函数表中。在计算机科学中,所有问题都可以通过添加另一层间接性来解决(除了过多的间接性)。

另请参见:

但是,TraitObjectdata部分是否可以转换为结构体?

不安全的转换不行。特质对象不包含有关原始类型的任何信息。它只有一个包含内存地址的裸指针。您可以不安全地将其转换为&Foo&u8&(),但编译器和运行时数据都不知道它最初是什么具体类型。

Any trait实际上也通过跟踪原始结构的类型ID来实现这一点。如果您请求引用正确的类型,则trait将为您转换数据指针。

除了我描述的FooOrBar trait之外,是否有其他模式可以处理这种情况,即我们需要迭代一堆trait对象,但要稍微不同地处理其中一些对象?

如果您拥有这些特质,那么您可以将as_foo添加到Bar特性中,反之亦然。 您可以创建一个枚举,其中包含Box<dyn Foo>Box<dyn Bar>,然后进行模式匹配。 您可以将bar的主体移动到该实现的foo的主体中。 您可以实现第三个特性Quux,其中调用<FooStruct as Quux>::quux会调用Foo::foo,而调用<BarStruct as Quux>::quux会调用Bar::foo,然后是Bar::bar

但是“TraitObject”的“data”部分是否可以转换为“struct”,然后我们可以安全地转换为其他trait?即使那样可行,它对于稳定的rust也不起作用。因此,除了我描述的“FooOrBar”trait之外,是否有其他模式可以处理这种情况,即我们需要迭代一堆trait对象,但略微不同地处理其中一些? - Christoph
2
我想指出的是,除了常规虚方法列表外,C++ 也使用 v-table 存储运行时类型信息,因此仅使用 v-table 并不排除向下转换的可能性。只是 Rust 不会存储此类信息。 - Matthieu M.

3

我做了以下事情。

我向 Foo trait 添加了一个名为 as_bar 的方法,它返回一个 Option<&Bar>。 我给 trait 添加了一个默认实现来返回 None,这样对于不关心 BarFoo 实现者来说几乎没有什么不便之处。

trait Foo {
    fn do_foo(&self);

    fn as_bar(&self) -> Option<&dyn Bar> {
        None
    }
}

我重写了实现FooBar接口的SomeFooBar结构体中的该方法,使其返回Some(self)

impl Foo for SomeFooBar {
    fn do_foo(&self) {
        println!("doing foo");
    }

    fn as_bar(&self) -> Option<&dyn Bar> {
        Some(self)
    }
}

这使调用代码看起来基本上符合我的期望。
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(bar) = foo.as_bar() {
            bar.do_bar();
        }
    }
}

我希望在未来Rust能够改进这一部分,但对于我的情况,这是一个完全可以接受的解决方案。

Playground


2

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


// 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,但你不能将其转换为Box<Foo>,因此我们并没有真正将一个特征转换为另一个特征)。

有趣的方法,但正如您所指出的那样,它并不完全符合我的需求,因为我必须决定是 Foo 还是 Bar,但不能同时将类型处理为 FooBar - Christoph

2
简而言之,目前该语言对于向下转型的支持非常有限。
长答案是,由于技术和哲学原因,能够进行向下转换并不被视为高优先级的问题:
  • 从技术角度来看,对于大多数情况,如果不是全部情况,都有解决方法。
  • 从哲学角度来看,向下转换会导致软件更加脆弱(因为您会意外地开始依赖于实现细节)。
有过多个提案,我自己也参与了其中一些,但目前还没有被选中的提案,也不清楚 Rust 是否会支持向下转换,如果支持,它的限制将是什么。
在此期间,您基本上有两种解决方法:
  1. 使用 TypeId:每种类型都有一个关联的 TypeId 值,可以查询该值,然后可以构建一个类型擦除容器,例如 Any 并查询它所持有的类型是否是特定的 X。在幕后,Any 将简单地检查存储的值的 TypeId 是否与此 X 的 TypeId 相匹配。

  2. 创建一个特定的 trait,就像你所做的那样。

后者更加开放,特别是可以与 traits 一起使用,而前者仅限于具体类型。


很有趣。你能详细说明一下TypeId部分吗?也许可以用一些代码来演示,我认为我还没有完全理解这个解决方法应该如何工作。 - Christoph
@Christoph:我链接了std::any::Any,它在后台使用TypeId,或者你可以查看我链接的GitHub存储库,了解如何基于TypeId创建自己的向下转型工具...但实际上,我只是为了完整性而提到这两个。Any不允许向下转型,它只允许检索原始类型,而rust-poly只是一个概念验证。目前唯一的通用解决方法是在trait中构建向下转型,这就是Servo所做的。 - Matthieu M.
好的,谢谢。我希望在未来看到Rust在这方面有所改进。与此同时,我已经更新了我的问题,并提供了解决方案。 - Christoph
@Christoph:啊!请不要在问题中混淆问题和答案!相反,您可以完美地发布自己的问题答案,并提供解决方案;我认为您选择的解决方案确实非常惯用。但是,值得一提的是,我本人并不喜欢向下转换(因为它会引入脆弱性),所以我不会想念它,并且非常希望永远不要使用它。 - Matthieu M.
抱歉,我改正了 ;) 实际上,我也不经常需要向下转型。我会看看没有它我能否处理好 ;) - Christoph

0
我最初找到的唯一解决方案是引入第三个特征 FooOrBar 并实现显式转换方法,然后将这个特征应用于两种类型。但这似乎不是处理该问题的正确工具。
trait FooOrBar {
    fn to_bar(&self) -> Option<&dyn Bar>;
    fn to_foo(&self) -> Option<&dyn Foo>;
}

impl FooOrBar for SomeFooBar {
    fn to_bar(&self) -> Option<&dyn Bar> {
        Some(self)
    }

    fn to_foo(&self) -> Option<&dyn Foo> {
        None
    }
}

impl FooOrBar for SomeFoo {
    fn to_bar(&self) -> Option<&dyn Bar> {
        None
    }

    fn to_foo(&self) -> Option<&dyn Foo> {
        Some(self)
    }
}

fn main() {
    let foos: Vec<Box<dyn FooOrBar>> = vec![Box::new(SomeFoo), Box::new(SomeFooBar)];

    for foo in foos {
        foo.to_foo().map(|foo| foo.do_foo());
        foo.to_bar().map(|foo| foo.do_bar());
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接