英文原文地址: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,以及类似下面的函数
fn func<T: Foo + ?Sized>(x: &T) { ... }当 object: &Foo,即 T 被赋予动态大小类型 Foo 时,如果能够像 func(object) 一样调用函数,将会非常好。你可能会猜到,如果没有对象安全概念,不可能这么做:任意的代码片段 ... 可以做不受控制的坏事。
在 trait object 上不能调用泛型方法。我们定义一个类似下面的 trait 和函数
trait Bad { fn generic_method<A>(&self, value: A);}
fn func<T: Bad + ?Sized>(x: &T) { x.generic_method("foo"); // A = &str x.generic_method(1_u8); // A = u8}当 obj 是 trait object &Bad 时,不能像 foo(obj) 一样调用函数 func,因为泛型方法调用不合法。几种可能的方式是:
Foo,<T: Foo + ?Sized>(x: &T) 之类的签名与 T = Foo 默认不能一起使用T = Bad 时,我们要求检查函数体是否合法&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 Bad::generic_method<Self: Bad + ?Sized, A>(self: &Self, x: a)如果 fn method(&self) 来自于 trait Foo,那么 x.method() 能被重写为 Foo::method(x)(自动解引用和自动引用,该过程可能添加一个 & 和/或若干个 *),如果没有对象安全,可能无法将 trait_object.method() 写为 Foo::method(trait_object)。对象安全通过禁止在不合法的场景下创建 trait object 的方式,保证这个转换(使 UFCS 与方法调用等价)总是合法。
在 RFC 546 和 PR 20341 之后,编译器隐式地创建 Foo(trait)的一个实现 Foo(类型),使 trait object 和这些种类的泛型函数可以一起使用。trait 的每个方法被实现为调用 vtable 中的相应方法。这种情况看起来像是:
trait Foo { fn method1(&self); fn method2(&mut self, x: i32, y: String) -> usize;}
// autogenerated implimpl<'a> Foo for Foo+'a { fn method1(&self) { // `self` is an `&Foo` trait object.
// load the right function pointer and call it with the opaque data pointer (self.vtable.method1)(self.data) } fn method2(&mut self, x: i32, y: String) -> usize { // `self` is an `&mut Foo` trait object
// as above, passing along the other arguments (self.vtable.method2)(self.data, x, y) }}澄清:trait object 的 .vtable 和 .data 标记不能直接使用,因此上面的代码无法编译,这里只是为了说明真正的行为。
Selftrait Foo: Sized { fn method(&self);}trait Foo 继承自 Sized,这要求 Self 类型大小固定,因此 impl Foo for Foo 非法:类型 Foo 大小不固定,并且没实现 Sized。trait 默认 Self 可能大小不固定 - 本质上是一个 bound Self: ?Sized - 使更多 trait 对象安全。
self这不再是对象不安全的,但是在大小可能不固定的类型上(包括 trait object)无法调用这种方法。也就是说,可以定义拥有 self 方法的 trait,但是无法通过 trait object 调用这些方法。
trait Foo { fn method(self);}trait Foo { fn func() -> i32;}在类型 Foo 中,无法提供静态方法 func 的合理实现:
impl<'a> Foo for Foo+'a { fn func() -> i32 { // what goes here?? }}编译器不知道返回哪个 i32 值,并且它不能调用某个其它类型的 Foo::func 方法。所有情况都没有意义。
Self有两种基本方式引用 Self:
引用 Self 类型意味着:必须匹配 self 值的类型,而它的真正类型在编译时是未知的。比如:
xxxxxxxxxxtrait Foo { fn method(&self, other: &Self);}两个参数的类型必须匹配,但是使用 trait object 时无法保证:被擦除类型的两个单独的 &Foo 值可能不匹配:
xxxxxxxxxximpl<'a> Foo for Foo+'a { fn method(&self, other: &(Foo+'a)) (self.vtable.method)(self.data, /* what goes here? */) }}不能使用 other.data,因为 self.vtable 的 method 条目假定:两个指针指向相同的,特定类型。但是不能绝对保证 other.data 指向匹配数据。编译器也没有必要检测错误,因为即使检测到错误,也无法处理。
trait Foo { fn method<A>(&self, a: A);}在 Rust 中,泛型函数是单态的,也就是说,编译器为被用作泛型参数的每种类型创建函数的一个拷贝。尝试的实现可能类似:
xxxxxxxxxximpl<'a> Foo for Foo+'a { fn method<A>(&self, a: A) { (self.vtable./* ... huh ???*/)(self.data, a: A) }}vtable 是函数指针的静态结构体,我们必须从其中选择能够与任意类型 A 一起使用的函数指针。为此,必须为可能用到的每种类型预生成代码,然后填进上面的 huh,以选择正确的函数指针。可以隐式地向 trait 中添加全部方法:
xxxxxxxxxxtrait Foo { fn method_u8(&self); // A = u8 fn method_i8(&self); // A = i8 fn method_String(&self); // A = String fn method_unit(&self); // A = () // ...}在 vtable 结构体中,每个方法需要一个条目。这样做会严重”膨胀“,尤其是大多数方法不会被使用。
仅当能被用作泛型参数的可能类型的数量有限并且完全已知时,这种方式才有用,才能编写全部方法列表。