Skip to content

23.2 object safe

既然谈到 trait object 就不得不说一下 object safe 的概念。trait object 的构造是受到许多约束的,当这些约束条件不能满足的时候,会产生编译错误。

我们来看看在哪些条件下 trait object 是无法构造出来的。

1.当 trait 有 Self:Sized 约束时

一般情况下,我们把 trait 当作类型来看的时候,它是不满足 Sized 条件的。因为 trait 只是描述了公共的行为,并不描述具体的内部实现,实现这个 trait 的具体类型是可以各种各样的,占据的空间大小也不是统一的。Self 关键字代表的类型是实现该 trait 的具体类型,在 impl 的时候,针对不同的类型,有不同的具体化实现。如果我们给 Self 加上 Sized 约束:


rust
#![feature(dyn_trait)]
trait Foo where Self: Sized {
    fn foo(&self);
}

impl Foo for i32 {
    fn foo(&self) { println!("{}", self); }
}

fn main() {
    let x = 1_i32;
    x.foo();
    //let p = &x as &dyn Foo;
    //p.foo();
}

我们可以看到,直接调用函数 foo 依然是可行的。可是,当我们试图创建 trait object 的时候,编译器阻止了我们:


rust
error: the trait `Foo` cannot be made into an object [E0038]

所以,如果我们不希望一个 trait 通过 trait object 的方式使用,可以为它加上Self: Sized约束。

同理,如果我们想阻止一个函数在虚函数表中出现,可以专门为该函数加上Self:Sized约束:


rust
#![feature(dyn_trait)]
trait Foo {
    fn foo1(&self);
    fn foo2(&self) where Self: Sized;
}

impl Foo for i32 {
    fn foo1(&self) { println!("foo1 {}", self); }
    fn foo2(&self) { println!("foo2 {}", self); }
}

fn main() {
    let x = 1_i32;
    x.foo2();
    let p = &x as &dyn Foo;
    p.foo2();
}

编译以上代码,可以看到,如果我们针对 foo2 函数添加了Self: Sized约束,那么就不能通过 trait object 来调用这个函数了。

2.当函数中有 Self 类型作为参数或者返回类型时

Self 类型是个很特殊的类型,它代表的是 impl 这个 trait 的当前类型。比如说,Clone 这个 trait 中的 clone 方法就返回了一个 Self 类型:


rust
pub trait Clone {
    fn clone(&self) -> Self;
    fn clone_from(&mut self, source: &Self) { ... }
}

我们可以想象一下,假如我们创建了一个 Clone trait 的 trait object,并调用 clone 方法:


rust
let p: &dyn Clone = if from_input() { &obj1 as &dyn Clone } else { &obj2 as &dyn Clone };
let o = p.clone();

变量o应该是什么类型?编译器不知道,因为它在编译阶段无法确定。p指向的具体对象,它的类型是什么只能在运行阶段确定,无法在编译阶段确定。在编译阶段,我们知道的仅仅是这个类型实现了 Clone trait,其他的就一无所知了。 而这个clone()方法又要求返回一个与p指向的具体类型一致的返回类型,所以o的类型是无法确定的。对编译器来说,这是无法完成的任务。所以,std::clone::Clone这个 trait 就不是 object safe 的,我们不能利用&dyn Clone构造 trait object 来实现虚函数调用。

编译下面的代码:


rust
#![feature(dyn_trait)]
fn main() {
    let s = String::new();
    let p : &dyn Clone = &s as &dyn Clone();
}

编译器会提示错误:


rust
error: the trait `std::clone::Clone` cannot be made into an object

Rust 规定,如果函数中除了 self 这个参数之外,还在其他参数或者返回值中用到了 Self 类型,那么这个函数就不是 object safe 的。这样的函数是不能使用 trait object 来调用的。这样的方法是不能在虚函数表中存在的。

这样的规定在某些情况下会给我们造成一定的困扰。假如我们有下面这样一个 trait,它里面的一部分方法是满足 object safe 的,而另外一部分是不满足的:


rust
#![feature(dyn_trait)]
trait Double {
    fn new() -> Self;
    fn double(&mut self);
}

impl Double for i32 {
    fn new() -> i32 { 0 }
    fn double(&mut self) { *self *= 2; }
}

fn main() {
    let mut i = 1;
    let p : &mut dyn Double = &mut i as &mut dyn Double;
    p.double();
}

编译会出错,因为new()这个方法是不满足 object safe 条件的。但是我们其实只想在 trait object 中调用 double 方法,并不指望通过 trait object 调用new()方法,但可惜编译器还是直接禁止了这个 trait object 的创建。

面对这样的情况,我们应该怎么处理呢?我们可以通过下面的写法,把new()方法从 trait object 的虚函数表中移除:


rust
fn new() -> Self where Self: Sized;

把这个方法加上Self: Sized约束,编译器就不会在生成虚函数表的时候考虑它了。生成 trait object 的时候,只需考虑double()这一个方法,编译器就会很愉快地创建这样的虚函数表和 trait object。通过这种方式,我们就可以解决掉一个 trait 中一部分方法不满足 object safe 的烦恼。

3.当函数第一个参数不是 self 时

意思是,如果有“静态方法”,那这个“静态方法”是不满足 object safe 条件的。这个条件几乎是显然的,编译器没有办法把静态方法加入到虚函数表中。

与上面讲解的情况类似,如果一个 trait 中存在静态方法,而又希望通过 trait object 来调用其他的方法,那么我们需要在这个静态方法后面加上Self: Sized约束,将它从虚函数表中剔除。

4.当函数有泛型参数时

假如我们有下面这样的 trait:


rust
trait SomeTrait {
    fn generic_fn<A>(&self, value: A);
}

这个函数带有一个泛型参数,如果我们使用 trait object 调用这个函数:


rust
fn func(x: &dyn SomeTrait) {
    x.generic_fn("foo"); // A = &str
    x.generic_fn(1_u8);  // A = u8
}

这样的写法会让编译器特别犯难,本来 x 是 trait object,通过它调用成员的方法是通过 vtable 虚函数表来进行查找并调用。现在需要被查找的函数成了泛型函数,而泛型函数在 Rust 中是编译阶段自动展开的,generic_fn函数实际上有许多不同的版本。这里有一个根本性的冲突问题。Rust 选择的解决方案是,禁止使用 trait object 来调用泛型函数,泛型函数是从虚函数表中剔除了的。这个行为跟 C++ 是一样的。C++ 中同样规定了类的虚成员函数不可以是 template 方法。

Released under the MIT License