我可以在两个特征之间进行转换吗?

Can I cast between two traits?

有没有办法从一个特征转换到另一个特征?

我有 FooBar 以及 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 反映了 trait 对象的实际实现方式。它们由两个指针组成: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 指向 table 个函数指针。 table 包含对每个已实现特征方法的引用,按某种编译器内部方式排序。

对于这个假设输入

trait Foo {
    fn one(&self);
}

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

table就是这样的伪代码

const FOO_U8_VTABLE: _ = [impl_of_foo_u8_one];

特征对象知道指向数据的指针和指向构成该特征的方法列表的指针。根据此信息,无法获取任何其他数据。

嗯,几乎没办法。正如您可能猜到的那样,您可以向 vtable 添加一个方法,即 returns 一个 different 特征对象。在计算机科学中,所有的问题都可以通过再增加一层间接来解决(除非间接层太多)。

另请参阅:

  • Why doesn't Rust support trait object upcasting?

But couldn't the data part of the TraitObject be transmuted to the struct

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

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

Is there a pattern other than the one I described with my FooOrBar trait to handle such cases where we need to iterate over a bunch of trait objects but treat some of them slightly different?

  • 如果您拥有这些特征,那么您可以将 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

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

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

简短的回答是:目前语言中对向下转型的支持极其有限。


长话短说,出于技术和哲学原因,能够向下转换并不被视为高优先级:

  • 从技术角度来看,即使不是所有情况,也有大多数情况的解决方法
  • 从哲学的角度来看,向下转换会导致更脆弱的软件(因为您意外地开始依赖实现细节)

有多个提案,我本人 participated,但目前 none 已被选中,目前尚不清楚 Rust 是否会变得沮丧,或者它是否会做它的局限性.

与此同时,您基本上有两个解决方法:

  1. 使用TypeId:每个类型都有一个关联的TypeId值可以查询,然后可以构建一个类型擦除的容器如Any和查询它持有的类型是否是一个特定的 X。在幕后 Any 将简单地检查这个 X 的 TypeId 与存储值的 TypeId

  2. 像您一样创建特定的 trait

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

这是我所做的。

我向 Foo 特性添加了一个 as_bar 方法,return 是一个 Option<&Bar>。我为 return None 提供了一个默认实现,这样对于 Foo 不关心 Bar.

的实现者来说几乎没有什么不便
trait Foo {
    fn do_foo(&self);

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

我将实现 FooBarSomeFooBar 结构的方法覆盖为 return 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();
        }
    }
}

Playground

我希望看到 Rust 将来在这方面有所改进,但这是我完全可以接受的解决方案。

我最初找到的唯一解决方案是引入带有显式转换器方法的第三个特征 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());
    }
}