特点介绍
前面的章节读完后,就已经掌握了 rust 的基本使用,但是还没有讲过 rust 的编程范式,这一节我们来了解 rust 的面向对象特性,我们都知道面向对象包含三个特性:封装、继承、多态,下面就从这三个点来看 rust 是如何设计面向对象编程的
封装
下面我们使用封装特性,实现一个对外暴露几个简单方法的结构体,用于自动计算平均数:
// src/average.rs
// 定义一个结构体
pub struct AveragedCollection {
// 用于求平均值的数组
list: Vec<i32>,
// 当前list数组的平均值
average: f64,
}
impl AveragedCollection {
// 为使用方提供新建一个集合的方法
pub fn new() -> Self {
Self {
list: vec![],
average: 0 as f64,
}
}
// 为使用方提供添加一个数字的方法
pub fn add(&mut self, value: i32) {
self.list.push(value);
// 添加后立即计算平均数
self.update_average();
}
// 为使用方提供删除最后一项的方法
pub fn remove(&mut self) -> Option<i32> {
let result = self.list.pop();
match result {
Some(value) => {
// 删除后立即计算平均数
self.update_average();
Some(value)
}
None => None,
}
}
// 为使用方提供获取当前平均数的方法
pub fn average(&self) -> f64 {
self.average
}
// 内部用于更新平均数的方法
fn update_average(&mut self) {
let total: i32 = self.list.iter().sum();
self.average = total as f64 / self.list.len() as f64;
}
}
复制代码
简单使用 AveragedCollection 结构体:
// src/main.rs
mod average;
use average::AveragedCollection;
let mut ac = AveragedCollection::new();
// 添加一些数值
ac.add(1);
ac.add(2);
ac.add(3);
ac.add(4);
println!("average(): {}", ac.average());
// 2.5
// 删除一个数值
ac.remove();
println!("average(): {}", ac.average());
// 2
println!("field average: {}", ac.average); // 报错,不能访问私有字段
println!("field list: {:?}", ac.list); // 报错,不能访问私有字段
复制代码
上面代码中使用 pub 关键字来实现对细节的封装,只对外暴露 new、add、remove、average 方法。由于 list 字段不是对外公开的,所以未来我们改变集合的实现时,使用方也无需改动代码,例如我们可以在 list 字段上使用 HashSet<i32>代替 Vec<i32>。
继承
rust 并不提供传统意义上的继承,作为替代解决方案,我们可以使用 Rust 中的默认 trait 方法来进行代码共享:
trait Dog {
// 方法的默认实现
fn say(&self) {
println!("汪汪~")
}
}
struct Husky {}
impl Dog for Husky {}
let husky = Husky {};
// 使用方法的默认实现
husky.say() // 汪汪~
复制代码
多态
我们可以在 Rust 中使用泛型来构建不同类型的抽象,并使用 trait 约束来决定类型必须提供的具体特性。这也被称作限定参数化多态(bounded parametric polymorphism)。
下面示例创建了一个图形用户界面(Graphical User Interface,GUI)包。这个工具会遍历某个元素列表,并依次调用元素的 draw 方法来将其绘制到屏幕中,包里对外暴露了许多内置组件,比如 Button、Text。另外,包用户也可以创建自定义类型,例如,某些开发者可能会添加 Image,而另外某些开发者则可能会添加 Select。
定义 Draw trait
每一个图形都必须实现 Draw trait:
pub trait Draw {
fn draw(&self); // 将会在Screen的run方法中被调用
}
复制代码
定义 Screen
下面定义 Screen 结构体,包含的 components 属性用于存储要绘制的图形:
pub struct Screen<T: Draw> {
// 包含要绘制的图形,要求实现Draw trait
pub components: Vec<T>
}
impl<T: Draw> Screen<T> {
pub fn run(&self) {
// 依次调用每个组件的draw方法
for item in self.components.iter() {
item.draw()
}
}
}
复制代码
定义组件
实现了 Screen,下面来实现两个图形组件,Button 和 Text,并分别为他们实现 Draw trait:
// 按钮组件
pub struct Button {
pub width: u32,
pub height: u32,
pub label: String
}
// 实现Draw trait
impl Draw for Button {
fn draw(&self) {
println!("绘制一个Button")
}
}
// 文本组件
pub struct Text {
pub width: u32,
pub height: u32,
pub placeholder: String
}
// 实现Draw trait
impl Draw for Text {
fn draw(&self) {
println!("绘制一个文本");
}
}
复制代码
实现绘制逻辑
接下来初始化画布并定义图形组件:
let screen = Screen {
components: vec![
Text {
width: 100,
height: 100,
placeholder: String::from("请输入文本"),
},
Button { // 报错,预期一个Text结构体,却发现了一个Button
width: 100,
height: 100,
label: String::from("确认"),
},
]
};
screen.run()
复制代码
上面在 components 的成员中,第二个成员 Button 出现了编译错误,原因在于泛型参数一次只能被替代为一个具体的类型,上面代码中由于第一个类型是 Text 类型,所以当传入结构体 Button 时,产生了编译错误。
使用 trait 对象
对于上面问题,我们可以利用 trait 对象来解决,trait 对象允许在运行时填入多种不同的具体类型:
pub struct Screen {
// 这个动态数组的元素类型使用了新语法Box<dyn Draw>来定义trait对象
// 它被用来代表所有被放置在Box中且实现了Draw trait
pub components: Vec<Box<dyn Draw>>
}
复制代码
重新实现绘制逻辑:
let screen = Screen {
components: vec![
// 使用Box来符合Draw trait
Box::new(Text {
width: 100,
height: 100,
placeholder: String::from("请输入文本"),
}),
Box::new(Button {
width: 100,
height: 100,
label: String::from("确认"),
}),
]
};
screen.run()
// 绘制一个文本
// 绘制一个Button
复制代码
rust 的多态性体现在实现 run 方法的过程中并不需要知晓每个组件的具体类型,它仅仅调用了组件的 draw 方法,而不会去检查某个组件究竟是 Button 实例还是 Text 实例。通过在定义动态数组 components 时指定 Box<dyn Draw>元素类型,Screen 实例只会接收那些能够调用 draw 方法的值。
封面图:跟着Tina画美国
评论