rust 面向对象

94 阅读4分钟

一: 面向对象的特征

  • 封装 -- 将数据和函数关联到单一类型的概念单元中, 称为对象
  • 抽象 -- 将数据和函数成员隐藏起来,以隐藏对象的实现细节
  • 多态 -- 从不同的功能角度与对象进行交互的能力
  • 继承 -- 从其他对象继承数据和行为的能力

二: Rust使用的是组合

结构体不具备从别的结构体继承数据和行为的能力,当然可以模拟。

可以看一下,rust中文社区, 北海写的 Rust与面向对象(三) - Rust语言中文社区 (rustcc.cn)

使用方法封装

Rust 支持对象的概念。“对象”是一个与一些函数(也称为方法)相关联的结构体。

任何方法的第一个参数必须是与方法调用相关联的实例的引用。(例如 instanceOfObj.foo())。Rust 使用:

  • &self —— 对实例的不可变引用。
  • &mut self —— 对实例的可变引用。

方法是在一个有 impl 关键字的实现块中定义的:

impl MyStruct { 
    ...
    fn foo(&self) {
        ...
    }
}

选择性暴露

struct SeaCreature {
    pub name: String,
    noise: String,
}

impl SeaCreature {
    pub fn get_sound(&self) -> &str {
        &self.noise
    }
}

Trait 特质

Rust 支持多态的特性。 Trait允许我们将一组方法与结构体类型联系起来。

trait MyTrait {
   fn foo(&self);
   ...
}

当一个结构体实现一个 trait 时,它便建立了一个契约,允许我们通过 trait 类型与结构体进行间接交互(例如 &dyn MyTrait),而不必知道其真实的类型。

结构体实现 Trait 方法是在实现块中定义要实现的方法:

impl MyTrait for MyStruct { 
    fn foo(&self) {
        ...
    }
    ... 
}

Trait 自带方法

Trait 可以有已实现的方法。

这些函数并不能直接访问结构体的内部字段,但它可以在许多 trait 实现者之间共享行为。

Trait 继承

Traits 可以从其他 trait 继承方法。

trait NoiseMaker {
    fn make_noise(&self);
}

trait LoudNoiseMaker: NoiseMaker {
    fn make_alot_of_noise(&self) {
        self.make_noise();
        self.make_noise();
        self.make_noise();
    }
}

动态调度和静态调度

方法的执行有两种方式

  • 静态调度-- 当实例类型已知时, 我们直接知道要调用什么函数
  • 动态调度 -- 当实例类型未知时,我们必须想方法来调用正确的函数

当使用动态调度时,Rust 会鼓励你在你的 trait 类型前加上dyn,以便其他人知道你在做什么。

内存细节:

  • 动态调度的速度稍慢,因为要追寻指针以找到真正的函数调用。

struct SeaCreature {
    pub name: String,
    noise: String,
}

impl SeaCreature {
    pub fn get_sound(&self) -> &str {
        &self.noise
    }
}

trait NoiseMaker {
    fn make_noise(&self);
}

impl NoiseMaker for SeaCreature {
    fn make_noise(&self) {
        println!("{}", &self.get_sound());
    }
}

fn static_make_noise(creature: &SeaCreature) {
    // 我们知道真实类型
    creature.make_noise();
}

fn dynamic_make_noise(noise_maker: &dyn NoiseMaker) {
    // 我们不知道真实类型
    noise_maker.make_noise();
}

fn main() {
    let creature = SeaCreature {
        name: String::from("Ferris"),
        noise: String::from("咕噜"),
    };
    static_make_noise(&creature);
    dynamic_make_noise(&creature);
}

Trait 对象

当我们将一个对象的实例传递给类型为 &dyn MyTrait 的参数时,我们传递的是所谓的 trait 对象

Trait 对象允许我们间接调用一个实例的正确方法。一个 trait 对象对应一个结构。 它保存着我们实例的指针,并保有一个指向我们实例方法的函数指针列表。

内存细节:

  • 这个函数列表在 C++ 中被称为 vtable

处理未知大小的数据

当我们想把 Trait 存储在另一个结构中时,它们亦带来了一个有趣的挑战。 Trait 混淆了原始结构,因此它也混淆了原来的结构体的大小。在 Rust 中,在结构体中存储未知大小的值有两种处理方式。

  • 泛型(generics)——使用参数化类型创建已知类型的结构/函数,因此大小变成已知的。
  • 间接存储(indirection)——将实例放在堆上,给我们提供了一个间接的层次,让我们不必担心实际类型的大小,只需存储一个指向它的指针。不过还有其他方法!

泛型函数

Rust中的泛型与 Trait 是相辅相成的。 当我们描述一个参数化类型 T 时,我们可以通过列出参数必须实现的 Trait 来限制哪些类型可以作为参数使用。

在以下例子中,类型 T 必须实现 Foo 这个 Trait:

fn my_function<T>(foo: T)
where
    T:Foo
{
    ...
}

通过使用泛型,我们在编译时创建静态类型的函数,这些函数有已知的类型和大小,允许我们对其执行静态调度,并存储为有已知大小的值。

泛型函数简写

Rust 为由Trait 限制的泛型函数提供了简写方式

fn my_function(foo: impl Foo){
    ...
}

Box

Box 是一个允许我们将数据从栈上移到堆上的数据结构。

Box 是一个被称为智能指针的结构,它持有指向我们在堆上的数据的指针。

由于 Box 是一个已知大小的结构体(因为它只是持有一个指针), 因此它经常被用在一个必须知道其字段大小的结构体中存储对某个目标的引用。

Box 非常常见,它几乎可以被用在任何地方:

Box::new(Foo { ... })