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

本文介绍了Rust中的封装、继承和多态等面向对象特性,以及静态分发(staticdispatch)与动态分发(dynamicdispatch)的区别,重点讲解了TraitObject的实现原理和适用场景。


前言

这一篇简单介绍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 中不能调用。
学习 Rust 语言需要一个系统性的路径,因为它不仅是一门现代系统编程语言,还引入了独特的**所有权(Ownership)**、**借用(Borrowing)** 和 **生命周期(Lifetimes)** 等概念,这些对初学者来说可能比较陌生。以下是一个结构化的学习路径,并附带推荐资源和练习建议。 --- ### 📚 系统学习 Rust 的步骤 #### 第一步:环境搭建 安装 Rust 工具链(包括 `rustc` 编译器、`cargo` 包管理器 和 `rustup` 版本管理工具): ```bash # 官方推荐方式安装 curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh ``` 重启终端后验证: ```bash rustc --version cargo --version ``` --- #### 第二步:官方教程《The Rust Programming Language》("Rust Book") 📖 阅读 [https://doc.rust-lang.org/book/](https://doc.rust-lang.org/book/) 这是最权威、最适合从零开始的学习资料。 重点章节顺序: 1. **第1-3章**:Hello World、Cargo 使用、基本语法 2. **第4章**:**所有权与借用**(核心难点!必须精读) 3. **第5章**:结构体(Structs) 4. **第6章**:枚举(Enums)与模式匹配(match) 5. **第7章**:模块系统(mod、use) 6. **第8章**:集合类型(Vec、String、HashMap) 7. **第9章**:错误处理(Result 与 panic!) 8. **第10章**:泛型与 trait 9. **第11章**:测试 10. **第12章**:构建命令行程序(实战项目) 11. **第13章**:闭包与迭代器 12. **第15章**:智能指针(Box, Rc, RefCell 等) 13. **第16章**:并发编程(线程、消息传递、共享状态) 14. **第17章**:面向对象设计模式的 Rust 实现 15. **第18-19章**:高级 trait 用法与宏 > ✅ 建议每章都动手写代码,不要只看不练! --- #### 第三步:动手实践小项目 通过实际项目巩固知识: ##### 项目 1:命令行工具 —— 搜索单词在文件中出现的行 ```rust // src/main.rs use std::env; use std::fs; fn main() { let args: Vec<String> = env::args().collect(); if args.len() < 3 { eprintln!("用法: {} <查询> <文件路径>", args[0]); return; } let query = &args[1]; let file_path = &args[2]; match fs::read_to_string(file_path) { Ok(contents) => { for line in contents.lines() { if line.contains(query) { println!("{}", line); } } } Err(e) => eprintln!("读取文件失败: {}", e), } } ``` 运行: ```bash cargo run hello example.txt ``` 这个项目涵盖:命令行参数、文件 IO、错误处理、字符串操作。 --- ##### 项目 2:实现一个简单的 Web Server(第20章内容) 使用标准库中的 `std::net::TcpListener` 构建 HTTP 服务器原型: ```rust use std::io::{Read, Write}; use std::net::TcpListener; use std::thread; use std::time::Duration; fn main() -> std::io::Result<()> { let listener = TcpListener::bind("127.0.0.1:8080")?; println!("服务器运行在 http://127.0.0.1:8080"); for stream in listener.incoming() { let mut stream = stream?; thread::spawn(|| { handle_connection(stream); }); } Ok(()) } fn handle_connection(mut stream: impl Write + Read) { let mut buffer = [0; 1024]; stream.read(&mut buffer).unwrap(); let request = String::from_utf8_lossy(&buffer[..]); println!("请求:\n{}", request); let (status_line, filename) = if request.starts_with("GET / ") { ("HTTP/1.1 200 OK", "index.html") } else { ("HTTP/1.1 404 NOT FOUND", "404.html") }; let contents = match fs::read_to_string(filename) { Ok(c) => c, Err(_) => "<h1>页面未找到</h1>".to_string(), }; let response = format!( "{}\r\nContent-Length: {}\r\n\r\n{}", status_line, contents.len(), contents ); stream.write_all(response.as_bytes()).unwrap(); stream.flush().unwrap(); } ``` 创建 `index.html` 文件并启动服务测试。 --- #### 第四步:深入理解核心机制 - **所有权规则**: - 每个值都有一个所有者。 - 值在任一时刻只能有一个所有者。 - 所有者离开作用域时,值被丢弃(Drop)。 - **借用检查器**: ```rust fn main() { let s1 = String::from("hello"); let len = calculate_length(&s1); // 引用,不转移所有权 println!("长度为 {} 的字符串是 {}", len, s1); } fn calculate_length(s: &String) -> usize { s.len() } ``` - **可变引用限制**:同一时间只能有一个可变引用,且不能与不可变引用共存。 --- #### 第五步:进阶学习 | 主题 | 推荐资源 | |------|---------| | Async/Await 并发 | [Tokio 官方教程](https://tokio.rs/) | | Web 开发 | [Axum](https://github.com/tokio-rs/axum), [Actix-web](https://actix.rs/) | | ORM | [SeaORM](https://www.sea-orm.org/) 或 Diesel | | WASM | [wasm-bindgen](https://github.com/rustwasm/wasm-bindgen) | | CLI 工具 | [clap](https://docs.rs/clap/latest/clap/) | | 测试与 TDD | 单元测试、集成测试、文档测试 | --- #### 第六步:参与开源或贡献项目 - GitHub 上搜索标签 `good first issue` + `Rust` - 尝试为知名项目如 `ripgrep`, `fd`, `bat` 提交 PR - 参与 [Rust RFCs](https://github.com/rust-lang/rfcs) 讨论 --- #### 第七步:持续提升 - 阅读《Rust for Rustaceans》(适合已有基础者) - 学习 unsafe Rust 和 FFI(调用 C 库) - 理解编译器内部机制(MIR、borrow checker) --- ### ✅ 学习建议总结 | 阶段 | 目标 | |------|------| | 入门 | 完成 Rust Book 前12章 + 写几个 CLI 工具 | | 中级 | 掌握所有权、生命周期、泛型、trait object | | 高级 | 异步编程、并发安全、性能优化、unsafe 编程 | | 专家 | 贡献标准库、编写高性能库、跨平台系统开发 | --- ### 💡 工具推荐 - **IDE 支持**:VS Code + [rust-analyzer 插件](https://rust-analyzer.github.io/) - **格式化**:`cargo fmt` - **Lint**:`cargo clippy` - **文档生成**:`cargo doc --open` - **依赖管理**:`cargo add`, `cargo rm`(需安装 `cargo-edit`) --- ### ❗常见痛点及应对 | 问题 | 解决方案 | |------|----------| | “为什么这段代码编译不过?” | 多看编译器提示,Rust 错误信息非常友好 | | 生命周期标注难懂 | 初期让编译器推导,后期再手动标注 | | 太多 trait bound | 使用 `impl Trait` 简化返回类型 | | async 写法复杂 | 先掌握 `async fn main()` 和 `.await`,再学 `Pin`, `Future` | ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值