原创声明

英文原文地址:https://huonw.github.io/blog/2015/01/object-safety/


在 Rust 中,仅能从满足特定约束的 trait 创建 trait object,这些约束被统称为“对象安全”(object safety)。object safety 似乎是不必要的限制,本文将深度分析它为何存在以及相关的编译器行为。

动机

RFC 255 引入对象安全(object safety)的概念,其动机是:在更多的期望使用“静态的” Foo(trait) 泛型的地方,能使用动态的 trait object 类型 Foo(类型)。从某种意义上讲,这使得 trait 的两种使用方式 - 静态分发和动态分发 - 互相接近,有助于减少特殊处理。

该 RFC 推行的高级行为/约束是:仅当 trait Foo 是对象安全的时,才能从 Foo 创建 trait object - &Foo&mut Foo-。本文以借用 & trait object 为例,但是所讲的东西可以应用到任意 trait object。

接下来看一个关于对象安全的示例:假设我们有一个 trait Foo,以及类似下面的函数

object: &Foo,即 T 被赋予动态大小类型 Foo 时,如果能够像 func(object) 一样调用函数,将会非常好。你可能会猜到,如果没有对象安全概念,不可能这么做:任意的代码片段 ... 可以做不受控制的坏事。

在 trait object 上不能调用泛型方法。我们定义一个类似下面的 trait 和函数

obj 是 trait object &Bad 时,不能像 foo(obj) 一样调用函数 func,因为泛型方法调用不合法。几种可能的方式是:

  1. 对于任意 trait Foo<T: Foo + ?Sized>(x: &T) 之类的签名与 T = Foo 默认不能一起使用
  2. T = Bad 时,我们要求检查函数体是否合法
  3. 确保我们绝不会把 &Bad 传进 func

方式 1 是对象安全之前存在的做法,也是对象安全要解决的问题。方式 2 违背 Rust 的目标:仅需知道被调用的函数/方法的签名,即可对程序进行类型检查。也就是说,只要满足函数/方法签名,就可以调用它。无需对泛型的每个真正实例的内部代码进行类型检查,因为签名保证内部合法。

方式 3 是 Rust 通过对象安全带来的,确保当 T == Foo 时,不可能遭遇拥有 fn func<T: Foo + ?Sized>(x: &T) 签名的函数做坏事的场景。

对象安全和这些种类的函数签名适用于 UFCS(uniform function call syntax)。UFCS 允许在定义函数的类型/trait 的作用域下像普通,泛型函数一样调用方法,比如来自上面的 trait 的 UFCS 函数 Bad::generic_method 实际上拥有如下签名:

如果 fn method(&self) 来自于 trait Foo,那么 x.method() 能被重写为 Foo::method(x)(自动解引用和自动引用,该过程可能添加一个 & 和/或若干个 *),如果没有对象安全,可能无法将 trait_object.method() 写为 Foo::method(trait_object)。对象安全通过禁止在不合法的场景下创建 trait object 的方式,保证这个转换(使 UFCS 与方法调用等价)总是合法。


工作原理

RFC 546PR 20341 之后,编译器隐式地创建 Foo(trait)的一个实现 Foo(类型),使 trait object 和这些种类的泛型函数可以一起使用。trait 的每个方法被实现为调用 vtable 中的相应方法。这种情况看起来是:

澄清:trait object 的 .vtable.data 标记不能直接使用,因此上面的代码无法编译,这里只是为了说明真正的行为。


对象安全规则

Sized Self

trait Foo 继承自 Sized,这要求 Self 类型大小固定,因此 impl Foo for Foo 非法:类型 Foo 大小不固定,并且没实现 Sized。trait 默认 Self 可能大小不固定 - 本质上是一个 bound Self: ?Sized - 使更多 trait 对象安全。

By-value self

这不再是对象不安全的,但是在大小可能不固定的类型上(包括 trait object)无法调用这种方法。也就是说,可以定义拥有 self 方法的 trait,但是无法通过 trait object 调用这些方法。

Static method

在类型 Foo 中,无法提供静态方法 func 的合理实现:

编译器不知道返回哪个 i32 值,并且它不能调用某个其它类型的 Foo::func 方法。所有情况都没有意义。

References Self

有两种基本方式引用 Self

引用 Self 类型意味着:必须匹配 self 值的类型,而它的真正类型在编译时是未知的。比如:

两个参数的类型必须匹配,但是使用 trait object 时无法保证:被擦除类型的两个单独的 &Foo 值可能不匹配:

不能使用 other.data,因为 self.vtablemethod 条目假定:两个指针指向相同的,特定类型。但是不能绝对保证 other.data 指向匹配数据。编译器也没有必要检测错误,因为即使检测到错误,也无法处理。

Generic method

在 Rust 中,泛型函数是单态的,也就是说,编译器为被用作泛型参数的每种类型创建函数的一个拷贝。尝试的实现可能类似:

vtable 是函数指针的静态结构体,我们必须从其中选择能够与任意类型 A 一起使用的函数指针。为此,必须为可能用到的每种类型预生成代码,然后填进上面的 huh,以选择正确的函数指针。可以隐式地向 trait 中添加全部方法:

在 vtable 结构体中,每个方法需要一个条目。这样做会严重”膨胀“,尤其是大多数方法不会被使用。

仅当能被用作泛型参数的可能类型的数量有限并且完全已知时,这种方式才有用,才能编写全部方法列表。