假设我们希望设计一个 GUI 库,对于每一个组件,我们希望能调用 draw()
方法来显示。
对于传统的有“继承”特性的语言,可以让所有组件都继承自一个 Component
类,这个类有一个纯虚函数 draw()
,然后每个组件实现各自的 draw()
方法。
然而 Rust 没有继承的概念,如果用泛型来实现:
pub struct StaticScreen<T: Draw> {
pub components: Vec<T>,
}
impl<T: Draw> StaticScreen<T> {
pub fn show(&self) {
for component in self.components.iter() {
component.draw();
}
}
}
这样的话,StaticScreen
类型只能存放一种类型的组件。例如Button
。不能满足需求。
我们需要一个新的方式来实现,即 Trait 对象。
定义 trait 对象
我们需要定义一个 Draw
trait,然后定义一个 vector,其中的对象都是实现了 Draw
trait 的。每个 trait 对象指向两个东西:
- 一个实现了
Draw
trait 的类型的实例 - (类似于 cpp 中的虚表)一个在 runtime 查找 trait 方法的表
trait 对象不能拥有数据成员,它的作用仅是允许抽象出公共的行为。 其定义方法是:
Box<dyn T>>
例如,对于这里的需求,可以如下定义:
pub struct DynamicScreen {
pub components: Vec<Box<dyn Draw>>,
}
impl DynamicScreen {
pub fn show(&self) {
for component in self.components.iter() {
component.draw();
}
}
}
这样,components
就是一个由实现了 Draw
trait 的智能指针构成的 vector。可以如下使用:
use gui::{DynamicScreen, Button, SelectBox};
fn main() {
let screen = DynamicScreen {
components: vec![
Box::new(SelectBox {
width: 75,
height: 10,
options: vec![
String::from("Yes"),
String::from("Maybe"),
String::from("No"),
],
}),
Box::new(Button {
width: 50,
height: 10,
label: String::from("OK"),
}),
],
};
screen.show();
}
可以看出,screen
中保存了两个类的实例,分别是 Button
和 SelectBox
。他们的共同特点是都实现了 Draw
trait。运行时会调用各自的 draw()
函数。
// component definition
pub struct Button {
pub width: u32,
pub height: u32,
pub label: String,
}
impl Draw for Button {
fn draw(&self) {
println!("Draw a button");
}
}
pub struct SelectBox {
pub width: u32,
pub height: u32,
pub options: Vec<String>,
}
impl Draw for SelectBox {
fn draw(&self) {
println!("Draw a select box");
}
}
结果是:
Draw a select box
Draw a button
Rust 中,我们并不关心 Button
和 SelectBox
之间有什么关系,只要他们都实现了 draw()
方法,就可以调用。这个思路和 golang 相似,都是基于 duck typing 的概念——如果它走路像样子,叫声也像鸭子,那它就是鸭子。
这样做的优势是我们不需要在运行时去检查是否实现了一个方法(cpp 中的 dynamic_cast
),如果没有实现 Draw
trait,它不会通过编译。
trait 对象的开销
trait 对象实际上起到了 cpp 中的虚函数的作用,因此也有类似的额外开销。
在上面的例子中,编译器无法在编译时知道要调用的对象的类型,因为这些对象是在一个动态数组中的,实际使用时,可能会根据用户输入变化。所以,它只能在运行时通过 trait 对象内部的指针来查找需要调用的方法。
网友评论