17Rust的面向对象编程特性

面向对象编程(Object-Oriented Programming, OOP)是一种模式化编程方式。

面向对象语言的特性

面向对象编程语言共享的特性:

对象、封装、继承

对象包含数据和行为

参考 Gang of Four 中对象的定义:

  • 面向对象的程序是由对象组成的。一个 对象 包含数据和操作这些数据的过程。这些过程通常被称为 方法操作
  • Rust 中的 结构体和枚举满足

封装encapsulation)的思想:对象的实现细节不能被使用对象的代码获取到。所以唯一与对象交互的方式是通过对象提供的公有 API;使用对象的代码无法深入到对象内部并直接改变数据或者行为。封装使得改变和重构对象的内部时无需改变使用对象的代码。

  • Rust 中不同部分使用pub 与否可以封装其实现细节。

Rust 中没有继承,

Rust 使用 trait 对象来实现多态

为使用不同类型的值而设计的 trait 对象

图形用户接口(Graphical User Interface, GUI)

定义通用行为的 trait

trait 对象(trait object)指向一个实现了指定 trait 的类型的实例,以及一个用于在运行时查找给类型的 trait 方法的表。

如何创建?

通过指定某种指针来创建 trait 对象,

  • 例如,&引用,或 Box<T> 智能指针,还有dyn 关键字,以及指定相关的 trait。

trait 对象

Box<dyn TRAIT类型>

1
2
3
4
5
6
7
8
pub trait Draw {
fn draw(&self);
}

pub struct Screen {
// 这里便是一个 trait 对象
pub components: Vec<Box<dyn Draw>>,
}

我们可以使用 trait对象代替泛型或具体类型。

  • 任何使用 trait 对象的位置,Rust 的类型系统会在编译时确保任何在此上下文中使用的值会实现其 trait 对象中指定的 trait。如此便无需在编译时就知晓所有可能的类型。

trait 对象的具体作用是:

  • 允许对通用行为进行抽象;
  • 与一般意义上的对象不同,trait对象不允许添加数据;只是行为的抽象。

使用 trait 对象的优势:

  • 无需在运行时检查一个值是否实现了特定方法或担心在调用时因为值没有实现方式而产生错误。
    • Rust 编译器会检查值是否实现了 trait对象所需要的trait。

trait 对象 与 泛型+trait bound 的不同

泛型+trait bound 的组合,适用于只需要同质(相同类型)的集合,一次实例只能是一种类型,在编译时采用具体类型,进行单态化

  • 没有运行时性能开销

trait 对象,则可以包含多种实现了指定 trait 的类型,是一种多态化的手段。

  • 有运行时性能开销

trait 对象执行动态分发

Rust 实现泛型时,使用了单态化,即在编译时将使用了泛型类型参数的地方替换为了具体类型,

  • 单态化所产生的代码进行静态分发(static dispatch)。

静态分发(static dispatch),发生于编译器在编译时就知晓调用了什么方法的时候。

动态分发(dynamic dispatch),编译器在编译时无法知晓调用了什么方法,而是在运行时确定调用了什么方法的代码。

使用 trait 对象时,Rust 必须使用动态分发。

  • 编译器无法知晓所有可能用于 trait 对象代码的类型,所以也就不知道应该调用哪个类型的哪个方法实现。
  • 同时动态分发也会阻止编译器有选择的内联方法代码,这会相应的禁用一些优化。
  • 优点就是编写代码更加灵活。
  • 所以需要在灵活性与性能上做权衡。

trait 对象要求对象安全

只有 对象安全object safe)的 trait 才可以组成 trait 对象。

如果一个 trait 中所有的方法有如下属性时,则该 trait 是对象安全的:

  • 返回值类型不为 Self
    • 注意,&self&mut self 是可以的,只有 Self 是不可以的。
  • 方法没有任何泛型类型参数(generic type parameter);

Self 关键字是我们要实现的 trait 或方法的类型别名

对象安全对于 trait object 是必须的,因为一旦有了 trait 对象,就不再知晓实现该 trait 的具体类型是什么了。

如果 trait 方法返回具体的 Self 类型,但是 trait 对象忘记了其真正的类型,那么方法不可能使用已经忘却的原始具体类型。

同理对于泛型类型参数来说,当使用 trait 时其会放入具体的类型参数:此具体类型变成了实现该 trait 的类型的一部分。当使用 trait 对象时其具体类型被抹去了,故无从得知放入泛型参数类型的类型是什么。

下面这个标准库中的 Clone trait 不是对象安全的,因为其 clone方法的方法返回值是 Self

1
2
3
pub trait Clone {
fn clone(&self) -> Self;
}

String 实现了 Clone trait,当在 String 实例上调用 clone 方法时会得到一个 String 实例。类似的,当调用 Vec<T> 实例的 clone 方法会得到一个 Vec<T> 实例。

clone 的签名需要知道什么类型会代替 Self,因为这是它的返回值。

解决方法

1
2
3
4
5
// 下面的 trait 因为 foo 的返回值为 Self,所以 Trait 无法创建 trait对象
trait Trait {
fn foo(&self) -> Self;
// more functions
}

当 trait 中只有一些方法不是对象安全的,可以通过给该方法添加 where Self: Sized 约束。将其标记为对 trait object(trait 对象)不可用的形式,让该 trait 依然可以生产 trait object。

同时被标记的方法依然除了不能在 trait object 中使用,依然可以在该 trait 的其他 impl 实现中使用。

1
2
3
4
trait Trait {
fn foo(&self) -> Self where Self: Sized;
// more functions
}

foo()不能再被trait对象调用了,但是现在可以创建一个trait对象,并且可以调用任何对象安全的方法。

面向对象设计模式的实现

状态模式(state pattern)是一个面向对象的设计模式。

  • 该模式的关键在于一个值有某些内部状态,体现为一系列的状态对象,同时值的行为随着其内部状态而改变。
  • 状态对象,是共享功能的(有相同的功能/方法)
    • 在 Rust 中通过结构体(struct)和 trait 来实现,其他语言比如Java 可能通过对象和继承(或面向接口编程)来实现。
  • 每个状态对象对其自己的行为负责,并在应该转换为另一个状态时负责管理。
  • 保存状态对象的值不知道状态的不同行为,也不知道何时在状态之间转换。
    • 这里或许表示为,保存状态对象的值不关心,或无感知状态的不同行为和状态间转换。
  • 当程序业务需求改变时,只需要更新某个状态对象内的代码来更改它的规则或者添加更多的状态对象即可。
  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2022-2023 ligongzhao
  • 访问人数: | 浏览次数:

请我喝杯咖啡吧~

支付宝
微信