5分钟速读之Rust权威指南(三十四)面向对象

431 阅读5分钟

特点介绍

前面的章节读完后,就已经掌握了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代替Vec。

继承

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元素类型,Screen实例只会接收那些能够调用draw方法的值。

封面图:跟着Tina画美国