动态分发
本课属于 Coddy Rust 学习旅程的“面向对象编程”单元。第 45 课,共 61 课。
当你使用带有 trait 限定的泛型,例如 <T: Summary> 时,Rust 会在编译时确定确切的类型。这称为静态分发:编译器会为你使用的每种具体类型生成专门的代码。这样运行速度很快,但也有一个限制:一个变量只能保存一种特定类型。
如果你需要一个在运行时可以保存不同类型的变量,该怎么办?这时就要用到特征对象。使用 Box<dyn Trait>,你可以存储任何实现了该特征的类型:
trait Speak {
fn speak(&self) -> String;
}
struct Dog;
struct Cat;
impl Speak for Dog {
fn speak(&self) -> String { String::from("Woof!") }
}
impl Speak for Cat {
fn speak(&self) -> String { String::from("Meow!") }
}
fn main() {
let animal: Box<dyn Speak> = Box::new(Dog);
println!("{}", animal.speak()); // "Woof!"
let animal: Box<dyn Speak> = Box::new(Cat);
println!("{}", animal.speak()); // "Meow!"
}
dyn 关键字表示动态分派:Rust 会在运行时查找要调用哪个方法,而不是在编译时查找。trait 对象的大小未知,因此需要使用 Box;Box 提供了一个大小固定的指针。
可以这样理解:泛型表示“我适用于类型 T”,而特征对象表示“我适用于任何能做到这件事的类型”。代价是少量运行时开销,换来通过同一个变量处理不同类型的灵活性。
挑战
简单让我们构建一个车辆租赁系统,来展示动态分发的强大之处!你将创建一个 trait,用来定义任何可租赁车辆能做什么,然后使用 Box<dyn Trait> 在同一个变量中存储不同类型的车辆。
你将把代码分成两个文件:
vehicle.rs:定义一个公开的Rentabletrait,其中包含一个rental_info方法,该方法接受&self并返回一个String。然后创建两个公开的结构体:Car:包含一个公开的model字段(String)。它的rental_info应返回Car: {model}Bike:包含一个公开的brand字段(String)。它的rental_info应返回Bike: {brand}
main.rs:引入你的车辆模块,并使用输入来演示动态分发。创建一个Box<dyn Rentable>变量,让它先存放一个Car,打印其租赁信息,然后将同一个变量重新赋值为存放一个Bike,并再次打印其租赁信息。
这里的关键在于,一个 Box<dyn Rentable> 类型的变量可以存放 Car 或 Bike:Rust 会在运行时确定要调用哪个 rental_info 方法。这就是动态分发的实际应用!
你的输出应显示两辆车的信息:
Car: {model}
Bike: {brand}例如,输入 Tesla Model 3 和 Trek 时:
Car: Tesla Model 3
Bike: Trek你会收到两个输入:车辆型号和自行车品牌。
亲手试试
mod vehicle;
use vehicle::{Rentable, Car, Bike};
fn main() {
// 读取输入
let mut car_model = String::new();
std::io::stdin().read_line(&mut car_model).expect("Failed to read line");
let car_model = car_model.trim().to_string();
let mut bike_brand = String::new();
std::io::stdin().read_line(&mut bike_brand).expect("Failed to read line");
let bike_brand = bike_brand.trim().to_string();
// TODO: 创建一个存放 Car 的 Box<dyn Rentable> 变量
// 使用 rental_info() 方法输出它的租赁信息
// TODO: 给同一个变量重新赋值,使其存放 Bike
// 再次输出它的租赁信息
}
这节课包含一个小测验。开始学习即可作答,并记录你的进度。
面向对象编程中的所有课
自己练习: Rust 在线编译器