面试题答案
一键面试类型擦除在动态派发中的发生过程
- 动态派发基础:在Rust中,动态派发通过trait对象实现。trait对象的类型是
&dyn Trait
或者Box<dyn Trait>
。例如:
trait Animal {
fn speak(&self);
}
struct Dog;
impl Animal for Dog {
fn speak(&self) {
println!("Woof!");
}
}
struct Cat;
impl Animal for Cat {
fn speak(&self) {
println!("Meow!");
}
}
fn main() {
let dog: Box<dyn Animal> = Box::new(Dog);
let cat: Box<dyn Animal> = Box::new(Cat);
dog.speak();
cat.speak();
}
- 类型擦除:当我们创建像
Box<dyn Animal>
这样的trait对象时,类型擦除就发生了。编译器只知道这个对象实现了Animal
trait ,但具体的类型(如Dog
或Cat
)被擦除了。这是因为trait对象的大小在编译时是未知的,Rust通过胖指针(fat pointer)来解决这个问题。胖指针包含两部分:一个指向数据的指针和一个指向vtable的指针。vtable是一个函数指针表,包含了该trait对象所实现的trait方法的地址。例如在上述代码中,Box<dyn Animal>
的胖指针指向Dog
或Cat
实例的数据,同时vtable指针指向Dog
或Cat
实现的Animal
trait方法。
Rust处理生命周期相关问题
- 生命周期标记:trait对象可以携带生命周期参数。例如:
trait Printable<'a> {
fn print(&self, s: &'a str);
}
struct Printer<'a> {
message: &'a str,
}
impl<'a> Printable<'a> for Printer<'a> {
fn print(&self, s: &'a str) {
println!("{}: {}", self.message, s);
}
}
fn main() {
let msg = "Hello";
let printer: Box<dyn Printable<'_>> = Box::new(Printer { message: msg });
printer.print("world");
}
这里Printable<'a>
trait 有一个生命周期参数'a
。Printer<'a>
结构体实现了该trait ,并且print
方法的参数s
也有生命周期'a
。在创建trait对象Box<dyn Printable<'_>>
时,'_
是生命周期省略语法,编译器会自动推断合适的生命周期。
2. 生命周期一致性:Rust确保trait对象的生命周期与它所使用的所有数据的生命周期一致。例如,如果我们有一个函数返回一个trait对象,这个trait对象的生命周期不能超过函数中局部变量的生命周期。如下代码编译会报错:
trait MyTrait {
fn do_something(&self);
}
fn create_trait_obj() -> Box<dyn MyTrait> {
struct LocalStruct;
impl MyTrait for LocalStruct {
fn do_something(&self) {
println!("Doing something");
}
}
let local = LocalStruct;
Box::new(local) // 报错:局部变量`local`在函数结束时被释放,而返回的trait对象的生命周期更长
}
在这个例子中,LocalStruct
是函数内的局部类型,返回的Box<dyn MyTrait>
不能有比create_trait_obj
函数调用更长的生命周期,因为local
在函数结束时会被释放。Rust通过严格的生命周期检查来避免悬空指针等内存安全问题。