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

18
以下代码中,无法从实现相同trait的动态大小类型的引用中获取trait对象的引用。为什么会这样?如果我可以使用&dyn Trait&(?Sized + Trait)来调用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`

1
&dyn Trait 是一个带有额外动态分发信息的胖指针。而你的 some_foo 只是一个普通的引用(指针)。类型的大小可以说明这个问题:Playground - turbulencetoo
1
我猜你永远无法进行这种类型的转换:请参见此基本示例。只有在具有“Sized”限制(删除“+?Sized”)时,转换才能成功。 - turbulencetoo
1
因为从技术上讲,我认为此刻 some_foo 已经拥有足够的信息来创建一个 trait 对象。如果它是 unsized 类型,那么它就是一个 fat pointer,并且已经包含了所需的 vtable 指针。如果它是 sized 类型,那么强制类型转换可以计算出 vtable 指针,因为在单态化期间它知道具体的类型,对吧? - w1th0utnam3
3
现在无法编写答案,但请考虑“impl FooTrait for [i32] {...}”。您仍然不能将“&[i32]”强制转换为“&dyn FooTrait”,因为“&[i32]”已经是一个fat指针;没有地方放置vtable。(您需要“obese pointers”来实现这一点。)注:其中的“fat pointer”和“vtable”指的是Rust语言中的“胖指针”和“虚函数表”。 - trent
2
@trentcl 很有趣。所以这不被支持是因为当您具有这些动态大小的类型的“更深嵌套”时,您可能会得到任意大的指针? - w1th0utnam3
显示剩余3条评论
3个回答

23
这个问题可以归纳为以下简单的例子(感谢turbulencetoo):
trait Foo {}

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

乍一看,这似乎应该编译,就像你观察到的那样:
- 如果T是Sized,则编译器在静态上知道应使用哪个vtable来创建特征对象; - 如果T是dyn Foo,则vtable指针是引用的一部分,可以直接复制到输出。 但是有第三种可能性会使事情变得复杂:
- 如果T是一些未定大小的类型,即使该特征是对象安全的,也没有impl Foo for T的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有一个虚函数表,它必须接受fat指针,因为impl可能使用在self上动态分派的Bar方法。
这会引起两个问题:
  • &dyn Foo对象中没有地方存储Bar虚函数表指针,因为其大小仅为两个指针(数据指针和Foo虚函数表指针)。
  • 即使你有两个指针,也不能混合使用"fat指针"虚函数表和"thin指针"虚函数表,因为它们必须以不同的方式调用。
因此,尽管dyn Bar实现了Foo,但无法将&dyn Bar转换为&dyn Foo
虽然切片(另一种unsized类型)没有使用虚函数表实现,但指向它们的指针仍然是fat的,因此相同的限制适用于impl Foo for [i32]
在某些情况下,您可以使用CoerceUnsized(仅适用于Rust 1.36的夜间版本)来表示类似"必须可强制转换为&dyn FooTrait"的约束。不幸的是,我不知道如何在您的情况下应用它。

另请参阅


3

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

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

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

无法提供默认实现(因为它要求 SelfSized,但将 FooTrait 约束为 Sized 就禁止了为其创建特质对象...)。
但是,对于所有的 Sized 实现,它都可以轻松地实现。
fn as_dyn(&self) -> &dyn FooTrait { self }

基本上,它将所有实现FooTrait的限制为具有大小,除了dyn FooTrait在Playground中尝试

为什么默认实现将 Self 约束为 Sized - Guerlando OCs
@guerlando-ocs,因为&dyn FooTrait只能创建适用于FooTraitSized实现。 - Pierre-Antoine

3

参考了这篇博客,该博客非常清楚地解释了fat指针。

感谢trentcl将问题简化为:

trait Foo {}

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

这涉及到如何在不同的 ?Sized 类型之间进行转换。
为了回答这个问题,让我们首先看一下未大小化类型 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的强制类型转换问题可以采用这个技巧

// 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");
    }
}

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