英文原文地址: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 impl
impl<'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
标记不能直接使用,因此上面的代码无法编译,这里只是为了说明真正的行为。
Self
trait 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
值的类型,而它的真正类型在编译时是未知的。比如:
xxxxxxxxxx
trait Foo {
fn method(&self, other: &Self);
}
两个参数的类型必须匹配,但是使用 trait object 时无法保证:被擦除类型的两个单独的 &Foo
值可能不匹配:
xxxxxxxxxx
impl<'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 中,泛型函数是单态的,也就是说,编译器为被用作泛型参数的每种类型创建函数的一个拷贝。尝试的实现可能类似:
xxxxxxxxxx
impl<'a> Foo for Foo+'a {
fn method<A>(&self, a: A) {
(self.vtable./* ... huh ???*/)(self.data, a: A)
}
}
vtable 是函数指针的静态结构体,我们必须从其中选择能够与任意类型 A
一起使用的函数指针。为此,必须为可能用到的每种类型预生成代码,然后填进上面的 huh
,以选择正确的函数指针。可以隐式地向 trait 中添加全部方法:
xxxxxxxxxx
trait 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 结构体中,每个方法需要一个条目。这样做会严重”膨胀“,尤其是大多数方法不会被使用。
仅当能被用作泛型参数的可能类型的数量有限并且完全已知时,这种方式才有用,才能编写全部方法列表。