Appearance
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 方法。