这一篇简单介绍Rust与面向对象,主要讨论静态分发和动态分发~
说到OOP就会提到其三大特性:封装、继承和多态。
通俗来说,封装就是只向外部暴露行为,将对象的数据和行为实现细节隐藏起来。
Rust的结构体和枚举类型可以包含数据,同时可以在impl
块中提供结构体类型和枚举类型的实现方法。虽然带有方法的结构体和枚举并不被称为对象,但这种设计还是可以提供与对象相似的功能。
Rust中默认情况下模块、类型、函数和方法都是私有的,除非使用pub
关键字修饰这个项,同时只是pub
修饰的项变为对上层可见,被修饰项的内部成员依然是私有的,除非也是用pub
关键字分别修饰它们。
根据上述可以认为Rust是拥有封装特性的。
继承的目的主要有两个:重用代码和实现多态。
Rust中没有继承,因为大部分的情况下可以使用组合的形式达到重用代码的目的。
如果想实现类似Java等面向对象语言基于继承形式的代码复用,可以在定义trait的时候提供默认方法,即将共有行为定义为一个trait。
多态表现为子类型可以用于父类型被使用的地方。
Rust中没有继承,但通过泛型和trait约束等技术对类型施加约束,实现被称为bounded parametric polymorpgism的多态(限定参数化多态)。
在传统的OOP语言如Java等中,多态往往是让子类继承父类或者多个类实现相同接口实现的。在Rust中,通过让多个类型都实现相同的trait,实现多态特性,此时只需要在需要用到相同trait的地方使用trait即可,不需要指定特定的类型,借助trait约束可以规定类型必须实现多个不同的trait。
作为一个长期使用Java的开发者,我觉得非常有必要理解清楚静态分发和动态分发的概念和使用,否则可能不知道如何使用Rust实现多态的时候,这也是Rust其中一个长期困扰我的点,当然我觉得对于C++开发者来说这可能不是什么太大的问题。
先说我对静态分发和动态分发的看法和结论:动态分发类似于Java中多态的行为,而静态分发实际上与Rust的泛型类似,特别是单态化。 如果有仔细看前面的文章 第N次入门Rust - 9.泛型、trait和生命周期 的话,相信会发现泛型和静态分发在语法设计上是统一的 (记得有大佬说过Rust在设计语法的时候会考虑概念或使用方式上的统一,目的是为了降低开发者的脑力负担)。
静态分发(static dispatch)指编译时确定值的具体类型,默认情况下编译器在编译泛型类型的代码时执行单态化,将泛型类型占位符替换为具体类型并编译。因为静态分发属于单态化,因此性能不会差。
静态分发的特点:编译会很慢(因为需要寻找具体的类型),生成的可执行文件会很大(因为单态化会使一份代码变成多份),但是程序运行会很快。
语法:
Self
代替类型名;MyTrait
,则当声明变量或者出入参等位置需要使用MyTrait
时,需要使用impl MyTrait
作为类型的占位符(我也不知道占位符这个说法准不准确)。示例:
use std::{fmt::Display};
trait MyTrait {
fn get_self(&self) -> &Self;
fn print(&self);
}
#[derive(Debug, Default)]
struct MyStruct {
value: String,
}
impl MyTrait for MyStruct {
fn get_self(&self) -> &Self { // 返回值类型也可以使用 &MyTrait
self
}
fn print(&self) {
print!("{:#?}", self)
}
}
impl Display for MyStruct {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.value)
}
}
fn exec_impl(ms: &impl MyTrait) -> &impl MyTrait{
ms.get_self()
}
fn main() {
let s1 : MyStruct = MyStruct::default();
let s2 : &impl MyStruct = exec_impl(&s1);
s2.print();
}
动态分发(dynamic dispatch)指编译时没有确定具体类型,只知道值是实现某些trait的类型(即这个值可以是任何实现了指定trait的类型)。此时这种类型被称为trait object(即具体类型需要等运行时才能知晓)。可以看出动态分配与Java中多态的行为类似。
使用动态分发在编译期无法确定调用的是那个类的方法,需要运行时才能知道,这会带来一些性能损耗,这需要权衡。
语法:
&dyn MyTrait
或者Box
,其中dyn
是关键字。MyTrait
,则当声明变量或者出入参等位置需要使用MyTrait
时,需要使用&dyn MyTrait
作为类型的占位符。pub fn format(input: &mut String, formatters: Vec<&dyn Formatter>) {
for formatter in formatters {
formatter.format(input);
}
}
示例:
use std::{fmt::Display};
trait MyTrait {
fn get_self(&self) -> &dyn MyTrait;
fn print(&self);
}
#[derive(Debug, Default)]
struct MyStruct {
value: String,
}
impl MyTrait for MyStruct {
fn get_self(&self) -> &dyn MyTrait {
self
}
fn print(&self) {
print!("{:#?}", self)
}
}
impl Display for dyn MyTrait {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "value={}", self)
}
}
fn exec_dyn(ms: &dyn MyTrait) -> &dyn MyTrait {
ms.get_self()
}
fn main() {
let s1 = MyStruct::default();
let s2 = exec_dyn(&s1);
s2.print();
}
destructor
:如何释放值,可以认为是析构方法?size
:类型的大小;alignment
:对齐方式;.xxx()
:函数指针,实际类型具体实现的方法,这里的xxx
是对应的方法名;如果 trait 所有的方法,返回值是 Self
或者携带泛型参数,那么这个 trait 就不能产生 trait object。
Self
,是因为 trait object 在产生时,原来的类型会被抹去,所以 Self
究竟是谁不知道。比如 Clone
trait 只有一个方法 clone()
,返回 Self
,所以它就不能产生 trait object。Self
或者使用了泛型参数,那么这部分方法在 trait object 中不能调用。