为什么不能将 `&(?Sized + Trait)` 转换为 `&dyn Trait`?

Why can't `&(?Sized + Trait)` be cast to `&dyn Trait`?

在下面的代码中,无法从对实现相同特征的动态大小类型的引用中获取对特征对象的引用。为什么会这样? &dyn Trait&(?Sized + Trait) 之间到底有什么区别,如果我可以同时使用它们来调用特征方法?

实现 FooTraitContainerTrait 的类型可能例如有 type Contained = dyn FooTraittype Contained = T,其中 T 是实现 FooTrait 的具体类型。在这两种情况下,获得 &dyn FooTrait 都是微不足道的。我想不出另一种情况,这是行不通的。为什么在 FooTraitContainerTrait 的一般情况下这不可能?

trait FooTrait {
    fn foo(&self) -> f64;
}

///

trait FooTraitContainerTrait {
    type Contained: ?Sized + FooTrait;
    fn get_ref(&self) -> &Self::Contained;
}

///

fn foo_dyn(dyn_some_foo: &dyn FooTrait) -> f64 {
    dyn_some_foo.foo()
}

fn foo_generic<T: ?Sized + FooTrait>(some_foo: &T) -> f64 {
    some_foo.foo()
}

///

fn foo_on_container<C: FooTraitContainerTrait>(containing_a_foo: &C) -> f64 {
    let some_foo = containing_a_foo.get_ref();
    // Following line doesn't work:
    //foo_dyn(some_foo)
    // Following line works:
    //some_foo.foo()
    // As does this:
    foo_generic(some_foo)
}

取消注释 foo_dyn(some_foo) 行会导致编译器错误

error[E0277]: the size for values of type `<C as FooTraitContainerTrait>::Contained` cannot be known at compilation time
  --> src/main.rs:27:22
   |
27 |     foo_dyn(contained)
   |             ^^^^^^^^^ doesn't have a size known at compile-time
   |
   = help: the trait `std::marker::Sized` is not implemented for `<C as FooTraitContainerTrait>::Contained`
   = note: to learn more, visit <https://doc.rust-lang.org/book/ch19-04-advanced-types.html#dynamically-sized-types-and-the-sized-trait>
   = help: consider adding a `where <C as FooTraitContainerTrait>::Contained: std::marker::Sized` bound
   = note: required for the cast to the object type `dyn FooTrait`

这个问题可以简化为以下简单示例(感谢 turbulencetoo):

trait Foo {}

fn make_dyn<T: Foo + ?Sized>(arg: &T) -> &dyn Foo {
    arg
}

乍一看,确实应该可以编译,正如您观察到的那样:

  • 如果TSized,编译器静态地知道它应该使用什么vtable来创建特征对象;
  • 如果 Tdyn Foo,vtable 指针是引用的一部分,可以直接复制到输出。

但是还有第三种可能性会阻碍工作:

  • 如果 T 不是 dyn Foo 的某种未确定大小的类型,即使特征是对象安全的,[=18 也没有 vtable =].

没有 vtable 的原因是具体类型的 vtable 假定 self 指针是瘦指针。当你调用一个dyn Trait对象的方法时,vtable指针用于查找一个函数指针,只有数据指针被传递给函数。

但是,假设您为未确定大小的类型实现了一个(对象安全的)特性:

trait Bar {}
trait Foo {
    fn foo(&self);
}

impl Foo for dyn Bar {
    fn foo(&self) {/* self is a fat pointer here */}
}

如果 这个 impl 的 vtable,它必须接受 fat 指针,因为 impl 可以使用 Bar 的方法,这些方法在 self.

上动态调度

这会导致两个问题:

  • Bar vtable 指针没有地方存储在 &dyn Foo 对象中,它的大小只有两个指针(数据指针和 Foo vtable 指针)。
  • 即使你有两个指针,你也不能混合和匹配 "fat pointer" vtables 和 "thin pointer" vtables,因为它们必须以不同的方式调用。

因此,即使 dyn Bar 实现了 Foo,也不可能将 &dyn Bar 变成 &dyn Foo

虽然切片(另一种未确定大小的类型)没有使用 vtables 实现,但指向它们的指针仍然很胖,因此同样的限制适用于 impl Foo for [i32]

在某些情况下,您可以使用 CoerceUnsized(仅适用于 Rust 1.36 的 nightly)来表示 "must be coercible to &dyn FooTrait" 之类的边界。不幸的是,我不知道如何在你的案例中应用它。

另见

  • Use trait object to pass str in rust 有一个引用未确定大小的类型 (str) 的具体示例,它不能被强制转换为对特征对象的引用。

不确定这是否解决了您的具体问题,但我确实使用以下技巧解决了我的问题:

我在FooTrait中添加了以下方法:

fn as_dyn(&self) -> &dyn FooTrait;

无法提供默认实现(因为它要求 SelfSized,但将 FooTrait 限制为 Sized 禁止为其创建特征对象。 ..).

然而,对于所有 Sized 实现,它被平凡地实现为

fn as_dyn(&self) -> &dyn FooTrait { self }

所以基本上它限制了 FooTrait 的所有实现的大小,除了 dyn FooTrait

Try it on the playground

引用自这个blog,它很好地解释了胖指针。

感谢 trentcl 将问题简化为:

trait Foo {}

fn make_dyn<T: Foo + ?Sized>(arg: &T) -> &dyn Foo {
    arg
}

这就带来了如何在不同的?Sized之间进行转换?

为了回答这个问题,让我们先看一下 Unsized 类型 Trait 的实现。

trait Bar {
    fn bar_method(&self) {
        println!("this is bar");
    }
}

trait Foo: Bar {
    fn foo_method(&self) {
        println!("this is foo");
    }
}

impl Bar for u8 {}
impl Foo for u8 {}

fn main() {
    let x: u8 = 35;
    let foo: &dyn Foo = &x;
    // can I do
    // let bar: &dyn Bar = foo;
}

那么,你能let bar: &dyn Bar = foo;吗?

// below is all pseudo code
pub struct TraitObjectFoo {
    data: *mut (),
    vtable_ptr: &VTableFoo,
}

pub struct VTableFoo {
    layout: Layout,
    // destructor
    drop_in_place: unsafe fn(*mut ()),
    // methods shown in deterministic order
    foo_method: fn(*mut ()),
    bar_method: fn(*mut ()),
}

// fields contains Foo and Bar method addresses for u8 implementation
static VTABLE_FOO_FOR_U8: VTableFoo = VTableFoo { ... };

从伪代码我们可以知道

// let foo: &dyn Foo = &x;
let foo = TraitObjectFoo {&x, &VTABLE_FOO_FOR_U8};
// let bar: &dyn Bar = foo;
// C++ syntax for contructor
let bar = TraitObjectBar(TraitObjectFoo {&x, &VTABLE_FOO_FOR_U8});

bar类型是TraitObjectBar,不是TraitObjectFoo类型。也就是说,你不能将一种类型的结构分配给另一种不同的类型(在 Rust 中,在 C++ 中你可以使用 reinterpret_cast)。

你可以做什么来获得另一个间接级别

impl Bar for dyn Foo {
...
}

let bar: &dyn Bar = &foo;
// TraitObjectFoo {&foo, &VTABLE_FOO_FOR_DYN_FOO}

同样的事情也适用于 Slice。

转换不同 Unsized 的解决方法可以通过这个 trick:

// blanket impl for all sized types, this allows for a very large majority of use-cases
impl<T: Bar> AsBar for T {
    fn as_bar(&self) -> &dyn Bar { self }
}

// a helper-trait to do the conversion
trait AsBar {
    fn as_bar(&self) -> &dyn Bar;
}

// note that Bar requires `AsBar`, this is what allows you to call `as_bar`
// from a trait object of something that requires `Bar` as a super-trait
trait Bar: AsBar {
    fn bar_method(&self) {
        println!("this is bar");
    }
}

// no change here
trait Foo: Bar {
    fn foo_method(&self) {
        println!("this is foo");
    }
}