Skip to content

面向对象编程特性

面向对象编程(Object-Oriented Programming,OOP)是一种对程序进行建模的方式.

面向对象语言的特征

对象包含数据和行为

面向对象的程序由对象组成.一个对象同时封装了数据以及操作这些数据的过程.这些过程通常被称为方法操作.

Rust 是面向对象的:结构体和枚举包含数据,而 impl 块提供了在结构体和枚举之上的方法.虽然带有方法的结构体和枚举并不被称为对象,但参考 The Gang of Four 中对象的定义,它们提供了与对象相同的功能.

封装

封装(Encapsulation):一个对象的实现细节对使用该对象的代码不可见.

就像一个微波炉——你只需要按"加热"键,不需要知道里面的磁控管是怎么工作的.微波炉把复杂的电路保护起来,只给你留几个安全的按钮.

对象交互的唯一方式是通过其公有 API;使用对象的代码不应能直接触及对象的内部并改变数据或行为.

Rust 的做法:

  • Rust 用 struct(结构体)把数据包起来.
  • 默认所有字段和方法都是私有的.想对外暴露,就加 pub 关键字.
  • Rust 没有传统的"类",它把数据(struct)和行为(impl)分开写,但逻辑上是一套.
rust
/// AveragedCollection 结构体维护了一个整型列表及其所有元素的平均值.
pub struct AveragedCollection {
  list: Vec<i32>,  // 私有字段
  average: f64,    // 私有字段
}

impl AveragedCollection {
  /// 创建一个新的空集合.
  pub fn new() -> AveragedCollection {
    AveragedCollection {
      list: vec![],
      average: 0.0,
    }
  }

  /// 向列表中添加一个元素,并更新平均值.
  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;
  }
}

公有方法 addremoveaverage 是访问或修改 AveragedCollection 实例中数据的唯一途径.listaverage 字段是私有的,外部代码无法直接修改,从而保证了 averagelist 的一致性.

继承

继承(Inheritance):一个对象可以从另一个对象的定义中继承元素,从而获得父对象的数据和行为,无需重复定义.

Rust 没有传统意义上的继承,无法在不借助宏的情况下让一个结构体继承另一个结构体的字段和方法.

但 Rust 通过 trait 默认实现 提供了类似继承的能力:

  • 任何实现该 trait 的类型,都会自动拥有默认方法实现(类似继承父类方法).
  • 也可以覆盖默认实现(类似子类重写父类方法).
rust
/// 定义 Summary trait,summarize 方法提供默认实现.
pub trait Summary {
  fn summarize_author(&self) -> String;

  fn summarize(&self) -> String {
    format!("(Read more from {}...)", self.summarize_author())
  }
}

pub struct NewsArticle {
  pub headline: String,
  pub location: String,
  pub author: String,
  pub content: String,
}

impl Summary for NewsArticle {
  fn summarize_author(&self) -> String {
    self.author.clone()
  }
  // summarize 使用默认实现,类似"继承"了 trait 中的方法.
}

pub struct Tweet {
  pub username: String,
  pub content: String,
}

impl Summary for Tweet {
  fn summarize_author(&self) -> String {
    format!("@{}", self.username)
  }

  // 覆盖默认实现,类似子类重写父类方法.
  fn summarize(&self) -> String {
    format!("{}: {}", self.username, self.content)
  }
}

多态

多态(Polymorphism):允许用同一接口处理不同类型的对象.

就像"一键触发,各显神通".你对一群动物喊"叫一声!",狗会汪汪,猫会喵喵——指令一样,但表现不同.

Rust 通过两种方式实现多态:

方式语法分发时机说明
泛型 + trait bound<T: Trait>编译时(静态分发)单态化,性能最优
trait object&dyn Trait / Box<dyn Trait>运行时(动态分发)可混合不同类型
  • 泛型 <T: Trait>:编译器为每种具体类型分别生成专属代码.这叫静态分发.
  • &dyn Trait:运行时通过虚函数表查找具体方法.这叫动态分发.

trait object 实现多态

Rust 使用 trait object&dyn TraitBox<dyn Trait>)实现运行时多态,而不是依赖继承体系.

静态分发:<T: Trait>

  • 语法: <T: Trait>
  • 编译期确定具体类型,编译器为每种类型生成一份专属代码.
  • 运行时无额外开销,但不能在同一集合中混放不同类型.

你要告诉我你是什么类型,而且你必须实现这个 trait.

rust
fn make_sound<T: Animal>(animal: T) {
  animal.make_sound();
}

make_sound(dog); // 编译器生成专门给 Dog 的函数
make_sound(cat); // 编译器生成专门给 Cat 的函数

动态分发:&dyn Trait

  • 语法: &dyn TraitBox<dyn Trait>
  • 本质: 一个胖指针,包含数据指针 + 虚函数表(vtable)指针
  • 运行时通过虚函数表(vtable)查找方法,有少量开销.
  • 可以在同一集合中混放不同类型,更加灵活.

我不管你是什么类型,只要你实现了这个 trait 就行,我会在运行时找到正确的方法.

rust
fn make_sound(animal: &dyn Animal) {
  animal.make_sound();
}

let animals: Vec<Box<dyn Animal>> = vec![Box::new(Dog), Box::new(Cat)];
for animal in &animals {
  make_sound(animal.as_ref()); // 同一函数,处理不同类型
}

注意:trait object 使用动态分发,会带来一定的运行时开销;泛型使用静态分发,性能更优,但无法在同一集合中存储不同类型.根据实际需求选择合适的方式.

静态 vs 动态分发对比

特性泛型 T: Trait(静态分发)Trait 对象 dyn Trait(动态分发)
分发时机编译时(零开销抽象)运行时(vtable 查表,有轻微开销)
同一集合只能放同一类型的元素可以放不同类型的元素
代码膨胀编译器为每种类型生成专属代码所有类型共用同一套代码
使用限制无额外限制Trait 必须满足对象安全规则

&Box

这是初学者最容易困惑的地方——为什么不能直接写 dyn Trait

  • 大小不固定:Rust 在编译时需要知道每个变量的大小.Dog 可能占 8 字节,Cat 可能占 800 字节.如果只说"给我个会叫的动物",Rust 不知道该分配多少内存.
  • 指针大小固定:无论指向什么,指针在 64 位系统上始终是 8 字节.
  • 胖指针(Fat Pointer):&dyn Trait 实际存储了两个指针:
    • 一个指向数据本身.
    • 一个指向虚函数表(vtable),记录了该类型所有 trait 方法的地址.
&dyn Trait 内存布局:
┌─────────────────┬─────────────────┐
│  data pointer   │  vtable pointer │
│  (指向实际数据)  │  (指向方法表)   │
└─────────────────┴─────────────────┘

trait object 限制

并非所有 trait 都能作为 trait object 使用,只有满足对象安全(Object Safety)的 trait 才可以.

一个 trait 是对象安全的,需满足:

  1. 方法的返回类型不是 Self,否则无法在运行时确定具体类型.
  2. 方法没有泛型参数,否则无法生成统一的虚函数表.

例如

rust
trait BadTrait {
    // 违背规则 1:返回 Self, 无法确定返回内存大小
    fn get_me(&self) -> Self;

    // 违背规则 2:带有泛型, 无法确定函数表(vtable)的大小
    fn say<T>(&self, msg: T);
}

// 编译器会报错
let x: Box<dyn BadTrait> = vec![]; 

设计模式的实现

面向对象模式(Object-Oriented Patterns): Rust 可以实现经典的面向对象设计模式,通常结合类型系统以更安全的方式表达.

状态模式(State Pattern): 状态模式是一种行为设计模式:一个值拥有内部状态,其行为随状态的改变而改变.

案例实现

首先我们将以一种更加传统的面向对象的方式实现案例,接着使用一种在 Rust 中更自然的方式.让我们使用状态模式来增量式地实现一个发布博文的工作流以探索这个概念.

最终功能看起来像这样:

  1. 博文从空白的草稿开始.
  2. 一旦草稿完成,请求审核博文.
  3. 一旦博文过审,它将被发表.
  4. 只有被发表的博文的内容会被打印,这样就不会意外打印出没有被审核的博文的文本.

任何其他对博文的修改尝试都不会生效.例如,如果尝试在请求审核之前通过一个草稿博文,博文应该保持未发布的状态.

面向对象模式

  • 使用 trait object 模拟传统面向对象的状态模式.
rust
// Post 表示一篇博文,包含一个状态和内容.
pub struct Post {
  state: Option<Box<dyn State>>,
  content: String,
}

// State trait 定义了状态转换和内容访问的方法.
trait State {
  // 请求审核,消耗 self,转换为待审核状态.
  fn request_review(self: Box<Self>) -> Box<dyn State>;
  // 审核通过,消耗 self,转换为已发布状态.
  fn approve(self: Box<Self>) -> Box<dyn State>;
  // 查看博客内容 默认实现:只有已发布状态才返回内容,其他状态返回空字符串.
  fn content<'a>(&self, _post: &'a Post) -> &'a str {
    ""
  }
}

struct Draft; // 草稿状态
struct PendingReview; // 待审核状态
struct Published; // 已发布状态

impl State for Draft {
  // 请求审核,草稿转换为待审核状态.
  fn request_review(self: Box<Self>) -> Box<dyn State> {
    Box::new(PendingReview)
  }
  // 审核通过,草稿不能直接发布,保持当前状态.
  fn approve(self: Box<Self>) -> Box<dyn State> {
    self
  }
}

impl State for PendingReview {
  // 请求审核,待审核状态保持不变.
  fn request_review(self: Box<Self>) -> Box<dyn State> {
    self
  }
  // 审核通过,待审核转换为已发布状态.
  fn approve(self: Box<Self>) -> Box<dyn State> {
    Box::new(Published)
  }
}

impl State for Published {
  // 请求审核,已发布状态保持不变.
  fn request_review(self: Box<Self>) -> Box<dyn State> {
    self
  }
  // 审核通过,已发布状态保持不变.
  fn approve(self: Box<Self>) -> Box<dyn State> {
    self
  }
  // 已发布状态返回博文内容.
  fn content<'a>(&self, post: &'a Post) -> &'a str {
    &post.content
  }
}

impl Post {
  // 创建一个新的草稿博文, 初始状态为 Draft.
  pub fn new() -> Post {
    Post {
      state: Some(Box::new(Draft)),
      content: String::new(),
    }
  }

  // 添加文本到博文内容, 无论当前状态如何都允许修改内容.
  pub fn add_text(&mut self, text: &str) {
    self.content.push_str(text);
  }

  // 请求审核,状态转换, 根据当前状态决定是否转换.
  pub fn request_review(&mut self) {
    if let Some(s) = self.state.take() {
      self.state = Some(s.request_review());
    }
  }

  // 审核通过,状态转换, 根据当前状态决定是否转换.
  pub fn approve(&mut self) {
    if let Some(s) = self.state.take() {
      self.state = Some(s.approve());
    }
  }

  // 获取博文内容,只有已发布状态才返回内容.
  pub fn content(&self) -> &str {
    if let Some(ref s) = self.state {
      s.content(self)
    } else {
      ""
    }
  }
}

每个状态都是实现的同一个 trait, 需要根据不同的状态实现不同的行为.需要为状态实现不需要的方法

状态模式

Rust 的惯用风格,通过类型来表达状态,非法的状态转换在编译期即会报错,更加安全.

rust
// 每个状态对应一个独立的类型,Post 结构体不再包含状态字段.

// 表示已发布的博文,包含内容.
pub struct Post {
  content: String,
}

// 表示草稿状态的博文,包含内容.
pub struct DraftPost {
  content: String,
}

// 表示待审核状态的博文,包含内容.
pub struct PendingReviewPost {
  content: String,
}

// 草稿状态的实现,提供添加文本和请求审核的方法.
impl DraftPost {
  // 创建一个新的草稿博文, 初始内容为空.
  pub fn new() -> DraftPost {
    DraftPost { content: String::new() }
  }
  // 添加文本到草稿博文内容.
  pub fn add_text(&mut self, text: &str) {
    self.content.push_str(text);
  }

  /// 请求审核,消耗 self,转换为 PendingReviewPost.
  pub fn request_review(self) -> PendingReviewPost {
    PendingReviewPost { content: self.content }
  }
}

// 待审核状态的实现,提供审核通过的方法.
impl PendingReviewPost {
  /// 审核通过,消耗 self,转换为已发布的 Post.
  pub fn approve(self) -> Post {
    Post { content: self.content }
  }
}

// 已发布状态的实现,提供获取内容的方法.
impl Post {
  pub fn content(&self) -> &str {
    &self.content
  }
}

// 使用示例:
// let mut post = DraftPost::new();
// post.add_text("今天天气很好.");
// let post = post.request_review();
// let post = post.approve();
// println!("{}", post.content());

状态分离,只给合法的状态提供相应方法,非法转换在编译期就会报错,例如,DraftPost 没有 approve 方法,无法直接从草稿状态跳过审核直接发布.

两种风格对比:

trait object 风格类型状态模式
状态检查时机运行时编译时
非法转换可能 panic编译报错
灵活性更高(状态可动态切换)较低(状态由类型决定)
Rust 惯用程度一般推荐

总结

Rust 不是传统意义上的面向对象语言,但通过结构体、impl 块、trait、trait object 等特性,完全能够实现封装、多态等核心 OOP 概念,并在类型安全和性能上更具优势.

OOP 概念Rust 实现方式
封装struct + pub / 私有字段
继承trait 默认实现
多态泛型 + trait bound(静态)/ dyn Trait(动态)
设计模式trait object 或类型状态模式

基于 MIT 协议发布