特点介绍
前面的章节读完后,就已经掌握了 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.rsmod 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 traitimpl Draw for Button { fn draw(&self) { println!("绘制一个Button") }}
// 文本组件pub struct Text { pub width: u32, pub height: u32, pub placeholder: String}// 实现Draw traitimpl 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画美国
评论