第N次入门Rust - 14.面向对象


前言

这一篇简单介绍Rust与面向对象,主要讨论静态分发和动态分发~


14.1 浅谈Rust中拥有的OOP特征

说到OOP就会提到其三大特性:封装、继承和多态。

14.1.1 封装

通俗来说,封装就是只向外部暴露行为,将对象的数据和行为实现细节隐藏起来。

Rust的结构体和枚举类型可以包含数据,同时可以在impl块中提供结构体类型和枚举类型的实现方法。虽然带有方法的结构体和枚举并不被称为对象,但这种设计还是可以提供与对象相似的功能。

Rust中默认情况下模块、类型、函数和方法都是私有的,除非使用pub关键字修饰这个项,同时只是pub修饰的项变为对上层可见,被修饰项的内部成员依然是私有的,除非也是用pub关键字分别修饰它们。

根据上述可以认为Rust是拥有封装特性的。

14.1.2 继承

继承的目的主要有两个:重用代码和实现多态。

Rust中没有继承,因为大部分的情况下可以使用组合的形式达到重用代码的目的。

如果想实现类似Java等面向对象语言基于继承形式的代码复用,可以在定义trait的时候提供默认方法,即将共有行为定义为一个trait。

14.1.3 多态

多态表现为子类型可以用于父类型被使用的地方。

Rust中没有继承,但通过泛型和trait约束等技术对类型施加约束,实现被称为bounded parametric polymorpgism的多态(限定参数化多态)。

在传统的OOP语言如Java等中,多态往往是让子类继承父类或者多个类实现相同接口实现的。在Rust中,通过让多个类型都实现相同的trait,实现多态特性,此时只需要在需要用到相同trait的地方使用trait即可,不需要指定特定的类型,借助trait约束可以规定类型必须实现多个不同的trait。

14.2 静态分发和动态分发

作为一个长期使用Java的开发者,我觉得非常有必要理解清楚静态分发和动态分发的概念和使用,否则可能不知道如何使用Rust实现多态的时候,这也是Rust其中一个长期困扰我的点,当然我觉得对于C++开发者来说这可能不是什么太大的问题。

先说我对静态分发和动态分发的看法和结论:动态分发类似于Java中多态的行为,而静态分发实际上与Rust的泛型类似,特别是单态化。 如果有仔细看前面的文章 第N次入门Rust - 9.泛型、trait和生命周期 的话,相信会发现泛型和静态分发在语法设计上是统一的 (记得有大佬说过Rust在设计语法的时候会考虑概念或使用方式上的统一,目的是为了降低开发者的脑力负担)。

14.2.1 静态分发(static dispatch)

静态分发(static dispatch)指编译时确定值的具体类型,默认情况下编译器在编译泛型类型的代码时执行单态化,将泛型类型占位符替换为具体类型并编译。因为静态分发属于单态化,因此性能不会差。

静态分发的特点:编译会很慢(因为需要寻找具体的类型),生成的可执行文件会很大(因为单态化会使一份代码变成多份),但是程序运行会很快。

语法:

  • trait声明时有可能入参或者出参需要使用具体的类型,此时可以使用Self代替类型名;
  • 假如trait的名字为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();
}

14.2.2 动态分发(dynamic dispatch)

动态分发(dynamic dispatch)指编译时没有确定具体类型,只知道值是实现某些trait的类型(即这个值可以是任何实现了指定trait的类型)。此时这种类型被称为trait object(即具体类型需要等运行时才能知晓)。可以看出动态分配与Java中多态的行为类似。

使用动态分发在编译期无法确定调用的是那个类的方法,需要运行时才能知道,这会带来一些性能损耗,这需要权衡。

语法:

  • 在使用泛型的位置使用&dyn MyTrait或者Box<dyn MyTrait>,其中dyn是关键字。
  • 假如trait的名字为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();
}

14.2.3 Trait Object

Trait Object实现原理

  • Trait Object 指的是当使用动态分发时实际在程序中代替trait使用的值。
  • Trait Object 的底层逻辑就是胖指针。其中,一个指针指向数据本身,另一个则指向虚函数表(vtable)。
  • vtable 是一张静态的表,Rust 在编译时会为使用了 trait object 的类型的 trait 实现生成一张表,放在可执行文件中(一般在 TEXT 或 RODATA 段)。vtable中保存的信息:
    • destructor:如何释放值,可以认为是析构方法?
    • size:类型的大小;
    • alignment:对齐方式;
    • .xxx():函数指针,实际类型具体实现的方法,这里的xxx是对应的方法名;

Trait Object结构如下:
Trait Object

不能使用动态分发的情况

如果 trait 所有的方法,返回值是 Self 或者携带泛型参数,那么这个 trait 就不能产生 trait object。

  • 不允许返回 Self,是因为 trait object 在产生时,原来的类型会被抹去,所以 Self 究竟是谁不知道。比如 Clone trait 只有一个方法 clone(),返回 Self,所以它就不能产生 trait object。
  • 不允许携带泛型参数,是因为 Rust 里带泛型的类型在编译时会做单态化,而 trait object 是运行时的产物,两者不能兼容。 比如 Fromtrait,因为整个 trait 带了泛型,每个方法也自然包含泛型,就不能产生 trait object。如果一个 trait 只有部分方法返回 Self 或者使用了泛型参数,那么这部分方法在 trait object 中不能调用。
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值