【问题标题】:Understanding Traits and Object Safety了解特征和对象安全
【发布时间】:2017-10-21 03:06:46
【问题描述】:

我在对象安全的基础知识上苦苦挣扎。如果我有这个代码

struct S {
    x: i32,
}

trait Trait: Sized {
    fn f(&self) -> i32
    where
        Self: Sized;
}

fn object_safety_dynamic(x: Trait) {}

我收到

error[E0038]: the trait `Trait` cannot be made into an object
  --> src/lib.rs:11:29
   |
5  | trait Trait: Sized {
   |       -----  ----- ...because it requires `Self: Sized`
   |       |
   |       this trait cannot be made into an object...
...
11 | fn object_safety_dynamic(x: Trait) {}
   |                             ^^^^^ the trait `Trait` cannot be made into an object

当我添加或删除 : Sized 作为超级特征或 f 的绑定时,我收到的错误消息略有不同。

谁能解释一下:

  • 为什么这个特定的例子不起作用? Trait Objects章节指出:

    那么是什么让方法对象安全?每个方法都必须要求Self: Sized

    这不是满足了吗?

  • Trait: Sizedwhere Self: Sized 有什么区别? (嗯,是的,一个继承 trait,另一个是参数绑定,但是从 Rust 的 trait 对象角度来看?

  • 为了使object_safety_dynamic 起作用,我必须首选进行哪些更改?

如果重要的话,我正在使用rustc 1.19.0-nightly (01951a61a 2017-05-20)

解决关于固定尺寸的评论。

trait TraitB {
    fn f(&self) -> i32
    where
        Self: Sized;

    fn g<T>(&self, t: T) -> i32
    where
        Self: Sized;
}

【问题讨论】:

    标签: rust traits trait-objects


    【解决方案1】:

    为什么这个特定的例子不起作用?章节Trait Objects 状态:

    那么是什么让方法对象安全?每个方法都必须要求Self: Sized

    这不是满足了吗?

    这个问题真的是:什么是特征对象

    trait 对象是面向对象范式中的一个接口:

    • 它公开了一组有限的方法,
    • 应用于未知的混凝土类型。

    应用操作的具体类型是未知的,这就是为什么要使用 trait 对象的原因,因为它允许以统一的方式操作一组异构的类型直到装配级别.

    具体类型未知,然而,意味着包含内存的内存区域的大小也是未知的;因此,trait 对象只能在 referencepointer 后面进行操作,例如 &amp;dyn TraitObject&amp;mut dyn TraitObjectBox&lt;dyn TraitObject&gt;

    在内存级别,它们中的每一个都以相同的方式表示:

    • 指向虚拟表的指针,它是一个结构,在固定偏移量处为特征对象的每个“方法”保存一个函数指针,
    • 指向对象实际数据的指针。

    Trait: SizedSelf: Sized 有什么区别? (嗯,是的,一个继承了 trait,另一个是参数绑定,但是从 Rust 的 trait 对象的角度来看?)

    Rust 中没有继承。在 both 情况下,这些都是 bounds

    • Trait: Sized 声明 trait 本身只能为已经实现 Sized 的类型实现,
    • fn method(&amp;self) where Self: Sized 声明只有实现 Sized 的类型才能实现此方法。

    注意:当实现一个 trait 时,所有方法最终都必须有一个定义;因此,后者只有在为具有Self: Sized 绑定的方法提供默认实现(如is shown here)时才真正有用。

    为了使object_safety_dynamic 起作用,我必须进行哪些首选更改?

    您必须通过引用或指针获取特征对象。使用引用还是指针取决于您是否要转移所有权。

    【讨论】:

    • 谢谢!我得到 1 和 3,但大约是 2:a) 如果我有一个 TraitB(见上文),由 5 种类型实现,在我的代码中与 .g&lt;T&gt;() 一起使用,其中 T 是 5 种其他类型,编译器将生成5*5=25 个 g 的实现,因为它无法静态确定实际需要哪一个? (如果是这样,在调用者未知的库中,这将如何工作?)和 b)为什么添加 where Self: Sized; 会改变这一切,因为它显然只适用于实现它的类型,而不是 T
    • 我刚刚意识到where Self: Sized 实际上排除了它在object_safety_dynamic 内部的使用......我想我从根本上误解了where
    • @left4bread:关于(a),它不起作用。如果要将每个 impl TraitB 块用作特征对象,则可以有一个 g 的实现。因此,特征对象的方法在类型上不是泛型的限制(它们可以在生命周期中是泛型的)。
    • @trentcl:确实,已修复:)
    【解决方案2】:

    使Trait 成为Sized 的超类型并没有帮助 - 事实上,正如错误消息所述,这是不允许的。 Trait 的每个实现仍然会有一个不同的 大小,所以你的函数object_safety_dynamic 不能被编译。这里不能使用单态化,因为没有泛型参数,所以编译后的函数必须适用于Trait所有实现。

    但是,引用 do 具有固定大小,因此将参数变为引用将起作用:

    trait Trait {
        fn f(&self) -> i32;
    }
    
    fn object_safety_dynamic(x: &Trait) {}
    

    特征对象是总是某种类型的引用,例如Box&lt;T&gt;&amp;T。这正是因为 trait 的实现大小会有所不同,而引用类型具有已知的固定大小。

    【讨论】:

    • 单态化不应该用于静态调度吗?他/她正在尝试进行动态调度,因此这无关紧要。事实上,通过单态化,它可以工作,因为创建了可以采用不同大小的不同函数。你的主要观点仍然是正确的。对不起,如果这是错误的;)
    • @H2O 你没有错。他并没有尝试单态化,但他也写了x: Trait 作为论点。动态调度必须使用某种引用。
    • 谢谢,但我和@H2O 有同样的问题......另外,这意味着我显然不能移动特征而只能使用借用/指针,为什么需要 Sized无论如何限制?编译器不能为任意类型处理这个吗?
    • @left4bread:不是真的。如果您有x: Trait,编译器无法知道x 有多大,因为它可以由任何大小的类型实现。它不知道要保留多少堆栈空间。它不知道结构字段的大小。它不知道Vec 的元素应该有多大。它可以使其正常工作的唯一方法是将其放在指针后面的堆上……现在我们又回到了原点。
    猜你喜欢
    • 1970-01-01
    • 2018-11-29
    • 2020-04-13
    • 1970-01-01
    • 2012-01-17
    • 2018-12-23
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多