【问题标题】:Why can't `&(?Sized + Trait)` be cast to `&dyn Trait`?为什么不能将 `&(?Sized + Trait)` 转换为 `dyn Trait`?
【发布时间】:2019-08-07 15:47:14
【问题描述】:

在下面的代码中,不可能从实现相同特征的动态大小类型的引用中获得对特征对象的引用。为什么会这样? &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`

【问题讨论】:

  • &amp;dyn Trait 是一个带有额外动态调度信息的胖指针。您的 some_foo 只是一个常规参考(指针)。类型的大小对此有所说明:Playground
  • 我猜你永远不会做这种类型的演员:see this basic example。只有绑定了Sized(删除+?Sized),转换才能成功。
  • 因为从技术上讲,目前我认为some_foo 有足够的信息来从中创建一个特征对象。如果它没有大小,则它是一个胖指针,并且已经包含所需的 vtable 指针。如果调整了大小,演员表可以计算 vtable 指针,因为它现在是单态化期间的具体类型,对吧?
  • 现在不能写答案,但考虑impl FooTrait for [i32] {...}。您仍然无法将&amp;[i32] 转换为&amp;dyn FooTrait,因为&amp;[i32] 已经是一个胖指针;没有地方可以放置 vtable。 (为此,您需要“肥胖指针”。)
  • @trentcl 有趣。所以这是不支持的,否则当你对这些动态大小的类型进行“更深的嵌套”时,你可能会得到任意大的指针?

标签: generics rust polymorphism trait-objects


【解决方案1】:

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

trait Foo {}

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

乍一看,正如您所观察到的,它确实应该编译:

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

但还有第三种可能性会影响工作:

  • 如果T 是一个 dyn Foo 的无大小类型,即使特征是对象安全的,impl Foo for T 也没有 vtable。

没有 vtable 的原因是因为具体类型的 vtable 假定 self 指针是细指针。在dyn Trait对象上调用方法时,vtable指针用于查找函数指针,只有数据指针传递给函数。

然而,假设你为一个无大小的类型实现了一个(n 对象安全)特征:

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 上动态调度。

这会导致两个问题:

  • &amp;dyn Foo 对象中无法存储Bar vtable 指针,该对象的大小只有两个指针(数据指针和Foo vtable 指针)。
  • 即使您有两个指针,也不能将“胖指针”vtable 与“瘦指针”vtable 混合和匹配,因为它们必须以不同的方式调用。

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

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

在某些情况下,您可以使用CoerceUnsized(仅在 Rust 1.36 的每晚)来表达诸如“必须强制转换为 &amp;dyn FooTrait”之类的界限。不幸的是,我不知道如何在您的情况下应用它。

另见

【讨论】:

    【解决方案2】:

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

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

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

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

    然而,对于所有Sized 实现,它被简单地实现为

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

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

    Try it on the playground

    【讨论】:

    • 为什么默认实现约束 SelfSized
    • @guerlando-ocs 因为只能为SizedFooTrait 实现创建&amp;dyn FooTrait
    【解决方案3】:

    引用自此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: &amp;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");
        }
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-04-06
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2022-01-19
      相关资源
      最近更新 更多