如何从trait对象中获取对具体类型的引用?

111

在这段代码中,我该如何从变量a中获取Box<B>&B或者&Box<B>

trait A {}

struct B;
impl A for B {}

fn main() {
    let mut a: Box<dyn A> = Box::new(B);
    let b = a as Box<B>;
}

这段代码返回一个错误:

error[E0605]: non-primitive cast: `std::boxed::Box<dyn A>` as `std::boxed::Box<B>`
 --> src/main.rs:8:13
  |
8 |     let b = a as Box<B>;
  |             ^^^^^^^^^^^
  |
  = note: an `as` expression can only be used to convert between primitive types. Consider using the `From` trait
2个回答

144

在Rust中进行向下转换有两种方法。第一种方法是使用Any。请注意,这仅允许您向下转换为确切的、原始的具体类型。只需按以下方式操作:

use std::any::Any;

trait A {
    fn as_any(&self) -> &dyn Any;
}

struct B;

impl A for B {
    fn as_any(&self) -> &dyn Any {
        self
    }
}

fn main() {
    let a: Box<dyn A> = Box::new(B);
    // The indirection through `as_any` is because using `downcast_ref`
    // on `Box<A>` *directly* only lets us downcast back to `&A` again.
    // The method ensures we get an `Any` vtable that lets us downcast
    // back to the original, concrete type.
    let b: &B = match a.as_any().downcast_ref::<B>() {
        Some(b) => b,
        None => panic!("&a isn't a B!"),
    };
}

另一种方法是在基本特征(在本例中为A)上为每个“目标”实现一个方法,并为每个所需的目标类型实现转换。
等等,我们为什么需要as_any 即使将Any添加为要求A的条件,它仍然无法正确工作。首先问题是在Box<dyn A>中的A也将实现Any...这意味着当您调用downcast_ref时,您实际上将在对象类型A上调用它。 Any只能向其调用的类型进行向下转换,在这种情况下是A,因此您只能将其转换回&dyn A,而您已经拥有它。
但是,其中包含底层类型的Any实现吗?好吧,是的,但您无法获取它。 Rust不允许您从&dyn A转换为&dyn Anyas_any就是为此而设计的;因为它仅在我们的“具体”类型上实现,所以编译器不会混淆应该调用哪一个。在&dyn A上调用它会导致它动态分派到具体的实现(在这种情况下是B::as_any),后者使用BAny实现返回&dyn Any,这正是我们想要的。
请注意,您可以通过根本不使用A来回避整个问题。 具体来说,以下内容也将有效:
fn main() {
    let a: Box<dyn Any> = Box::new(B);
    let _: &B = match a.downcast_ref::<B>() {
        Some(b) => b,
        None => panic!("&a isn't a B!")
    };    
}

然而,这将使你无法使用其他方法;在这里你只能将其向下转换为一个具体类型。
最后值得一提的是,mopa crate 可以让你将 Any 的功能与自己的 trait 结合起来。

2
值得指出的是为什么需要 as_any 函数。这个函数被实现在类型 B 中,它接受一个类型为 &B 的参数 self,该参数会被转换为一个 &Any 类型,然后可以再次转换回一个 &B 类型。如果将 a.as_any() 替换为 (&*a as &Any),那么它只能被转换回转换成 &Any 类型的那个类型,也就是 &A&A&B 不是同一种类型,因为它们有不同的虚表。 - dhardy
7
我已经寻找这个答案整整一天了。有时候Rust感觉非常违反直觉。 - fasih.rana
6
下一个使用 Google 的人:你可能更容易地使用 downcast-rs 这个包(https://crates.io/crates/downcast-rs)。 - Nate Glenn
“as_any” 的问题现在解决了吗?我刚刚遇到一个例子,在其中直接对 “Box” 进行了向下转型:https://doc.rust-lang.org/std/any/trait.Any.html#impl-Box%3Cdyn%20Any%20+%20Send%20+%20%27static%2C%20A%3E - jaques-sam
1
对于关于“downcast-rs”的评论,仅用于转换类型,我不想使用另一个crate。这必须使用纯Rust处理。 - yerlilbilgin

9
很明显,如果有另一种类型C实现A并且您尝试将Box<C>转换为Box<B>,则转换可能会失败。我不知道您的情况,但对我来说,它看起来很像您正在将其他语言(如Java)的技术带入Rust中。在Rust中,我从未遇到过这种问题--也许您的代码设计可以改进以避免这种类型的转换。
如果你愿意的话,你可以使用 mem::transmute 转换几乎任何东西。可惜的是,如果我们想要将 Box<A> 转换为 Box<B> 或者将 &A 转换为 &B,那么会出现问题,因为指向 trait 的指针是一个“胖指针”,实际上由两个指针组成:一个指向实际对象,另一个指向 vptr。如果我们将其转换为结构体类型,则可以忽略 vptr。请记住,这种解决方案非常不安全且相当繁琐 - 我不会在“真实”的代码中使用它。
let (b, vptr): (Box<B>, *const ()) = unsafe { std::mem::transmute(a) };

编辑:算了,这比我想象的更不安全。如果你想正确地做到这一点,你必须使用 std::raw::TraitObject。但这仍然不稳定。我认为这对OP没有任何用处;不要使用它!
在这个非常相似的问题中有更好的替代方法:如何匹配特质实现者

1
感谢您的回答。我正在努力理解如何应对这种情况。很高兴了解可能的解决方案。 - Aleksandr

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