如果用一句话来评价 Rust 的面向对象(OOP):Rust 并没有照搬 Java / C++ 的那套传统面向对象,而是"取其精华,去其糟粕"------它没有 class 关键字,但它把"数据(struct)"、"行为(impl)"和"接口多态(trait)"拆开重组成了一套更现代、更清爽的体系。
对比传统面向对象的三大特性,Rust 的态度非常鲜明:
|--------------------------|-----------------|---------------------------------------------------|
| 面向对象三大特性 | Rust 是否支持? | Rust 的实现方式 |
| 1. 封装(Encapsulation) | ✅ 完美支持 | 通过 mod、struct 字段默认私有 + pub 公开方法 |
| 2. 多态(Polymorphism) | ✅ 完美支持(且更强) | 通过 trait(特征/接口) 实现静态与动态多态 |
| 3. 继承(Inheritance) | ❌ 彻底砍掉! | 严禁结构体之间搞父子继承(extends),改用 "组合 + Trait 默认方法" 替代 |
下面我们分 4 个部分,带你一次性看懂怎么用 Rust 写出面向对象的代码:
一、 Rust 里的"类(Class)"怎么写?(struct + impl)
还记得前面看 struct User 时我们说的吗?在 Java/JS 里,属性和方法都塞在同一个 class User {} 大括号里;而 Rust 讲究**"数据与行为分离"**:
-
struct 块 :只管定义有哪些属性字段;
-
impl 块 :专门用来给这个结构体写方法(函数)。
看一个最标准的例子:
Rust
// 1. 【数据部分】:定义结构体(字段默认全私有,外部不能直接乱改)
pub struct BankAccount {
pub owner: String, // 户主名字公开
balance: f64, // 🔒 余额私有!防止外部直接写 account.balance = 9999999.0;
}
// 2. 【行为部分】:用 impl 关键字给 BankAccount 挂载方法
impl BankAccount {
// ① 关联函数(没有 self 参数):相当于构造函数,用双冒号 BankAccount::new() 调用
pub fn new(owner: String) -> Self {
Self { // Self 就是 BankAccount 的代称
owner,
balance: 0.0,
}
}
// ② 只读方法(第一个参数是 &self):只能读属性,不能改,用点号 . 调用
pub fn get_balance(&self) -> f64 {
self.balance
}
// ③ 修改方法(第一个参数是 &mut self):可以修改内部属性,用点号 . 调用
pub fn deposit(&mut self, amount: f64) {
if amount > 0.0 {
self.balance += amount; // 封装的好处:只能在合法的逻辑下修改余额!
}
}
}
fn main() {
// 用 :: 调用构造函数造出对象(因为后面要存钱修改,所以加 mut)
let mut my_acc = BankAccount::new(String::from("张三"));
// 用 . 调用实例方法(点号会自动帮你处理 &self 和 &mut self)
my_acc.deposit(100.0);
println!("{} 的当前余额: {}", my_acc.owner, my_acc.get_balance());
}
💡 核心细节 :在 impl 块里,只要函数的第一个参数写的是 &self 或 &mut self (相当于 Java/JS 的 this 或 Python 的 self),它就是实例方法 (用 对象.方法() 调用);如果不带 self,它就是静态关联函数(用 类型::函数() 调用)。
二、 多态(Polymorphism):核心灵魂 trait(接口)
之前你在 fn largest<T: PartialOrd> 里见过 PartialOrd,它就是一个 Trait(特征) 。
你可以把 Rust 的 trait 直接理解为 Java 里的 interface(接口)!
只要不同的结构体实现了同一个 trait,它们就能表现出"多态":
Rust
// 1. 定义一个接口(Trait):规定凡是"动物",都必须实现 speak 方法
pub trait Animal {
fn speak(&self);
}
// 2. 定义两个完全不相干的结构体:狗 和 猫
struct Dog;
struct Cat;
// 3. 用【impl 接口名 for 结构体名】分别实现这个接口!
impl Animal for Dog {
fn speak(&self) {
println!("旺财: 汪汪汪!");
}
}
impl Animal for Cat {
fn speak(&self) {
println!("咪咪: 喵喵喵~");
}
}
实现了同一个 Animal 接口后,Rust 提供了 2 种多态玩法:
玩法 A:静态多态(编译期展开,零成本,速度最快)
函数参数直接写 &impl Animal(意思是谁实现了 Animal 就能传进来):
Rust
fn let_it_speak(pet: &impl Animal) {
pet.speak();
}
fn main() {
let d = Dog;
let c = Cat;
let_it_speak(&d); // 输出:汪汪汪!
let_it_speak(&c); // 输出:喵喵喵~
}
玩法 B:动态多态(dyn Trait ------ 把不同类型塞进同一个数组!)
之前我们知道 Rust 的数组 Vec 要求里面的元素必须是同一种类型。那如果我想把 Dog 和 Cat 装进同一个动物园数组里一起遍历怎么办?用 Box<dyn Animal>(dyn 代表 Dynamic 动态多态):
Rust
fn main() {
// Box<dyn Animal> 意思是:"盒子里装的是什么具体类型我不管,只要它实现了 Animal 接口就行!"
let zoo: Vec<Box<dyn Animal>> = vec![
Box::new(Dog),
Box::new(Cat),
];
for pet in zoo {
pet.speak(); // 自动根据盒子里装的是狗还是猫,调用对应的 speak()!
}
}
三、 为什么 Rust 坚决砍掉了"类继承(extends)"?怎么替代?
在 Java / C++ 中,人们经常写 Cat extends Animal、Dog extends Animal。但近二十年的软件工程界达成了共识:深层的类继承是代码腐烂的万恶之源(父类改一个字段,底下几十个子类全崩;或者子类只想复用父类的一个函数,却被迫继承了父类十几个用不上的垃圾属性)。
Rust 彻底废除了结构体之间的 extends 继承,而是用更干净的两招来替代:
1. 想复用"数据字段"
→→
用"组合(Composition)"
不要让 Dog 继承 Animal,而是直接把公共属性做成组件塞进 Dog 肚子里:
Rust
struct BaseInfo {
name: String,
age: u8,
}
struct Dog {
info: BaseInfo, // 直接把基础信息作为一个字段组合进来!清晰明了!
breed: String,
}
2. 想复用"公共方法代码"
→→
用"trait 默认实现"
在 Rust 的 trait 接口里,不仅能声明空方法,还能直接把默认代码写好!任何结构体只要挂上这个 trait,不用写一行代码就能直接白嫖这个方法:
Rust
trait Animal {
// 直接在接口里写好默认实现!
fn sleep(&self) {
println!("呼呼大睡中... Zzz");
}
}
struct Dog;
impl Animal for Dog {} // 里面什么都不用写!直接白嫖 sleep 方法!
fn main() {
let d = Dog;
d.sleep(); // 直接能调!输出:呼呼大睡中... Zzz
}
四、 极其强大的杀手锏:可以给"别人的类型"强行加方法!
在 Java 或 C++ 里,如果标准库自带的 i32(整数)或 String 没有某个方法,你是绝对没法修改标准库源码去给 i32 加方法的。
但在 Rust 里,因为 struct 和 impl Trait 是彻底分离的,你甚至可以自己写个 trait,然后强行给系统自带的 i32 整数加上新技能!
Rust
// 1. 我自己定义一个接口
trait DoubleIt {
fn double(&self) -> i32;
}
// 2. 强行给系统内置的 i32 整数实现这个接口!
impl DoubleIt for i32 {
fn double(&self) -> i32 {
*self * 2
}
}
fn main() {
// 3. 见证奇迹:普通的数字 5 居然能直接用点号调用我写的 .double() 方法了!
println!("5 的两倍是: {}", 5.double()); // 输出: 10
}
总结:从传统 OOP 到 Rust 的思维转换
-
想写一个类(Class)
→→写一个 struct (放字段)+ 一个 impl (放方法)。
-
想写构造函数(new)
→→在 impl 里写 pub fn new() -> Self 。
-
想写接口/多态(Interface)
→→写一个 trait ,然后用 impl Trait名 for 结构体名 。
-
想复用代码(替代 extends)
→→字段复用靠结构体嵌套组合 ,方法复用靠 trait 默认方法 。
From Trait语法
看到 From Trait ,其实你今天已经跟它打了十几次交道了------从你发的第一张代码截图开始,每一处 String::from("runoob") 调的就是这个 From Trait!
一句话概括它的作用:From 是 Rust 标准库内置的一个"类型转换接口(Trait)",专门用来规定"如何把类型 A 转换成类型 B"。
我们结合你刚刚学的面向对象 impl Trait for Struct 语法,分 3 步 把它彻底扒开:
一、 破案时刻:String::from("hello") 到底是怎么来的?
在 Rust 标准库里,提前定义好了这样一个极其简单的接口(Trait):
Rust
pub trait From<T> {
fn from(value: T) -> Self; // 接收一个 T 类型的原料,造出并返回当前类型 Self
}
- 含义 :如果类型 B 实现了 From<A>(写作 impl From<A> for B),就代表 B 学会了"如何拿 A 当原料,把自己造出来"!
而我们写的双引号字符串字面量 "runoob",它的原始类型其实是只读切片 &str。
Rust 标准库的作者提前写了这样一段代码:
Rust
// 标准库源码:教 String 如何从 &str 转换而来
impl From<&str> for String {
fn from(s: &str) -> Self {
// 内部去堆内存申请空间,把 s 复制进去变成 String 返回
}
}
正因为标准库写了这个 impl From<&str> for String,所以你才能到处写 String::from("runoob"),把 &str 转换成 String!
二、 实战:给你自己的 User 结构体实现 From!
既然 From 只是一个公开的接口,那我们完全可以给自己定义的 User 结构体也实现它!
比如,我想直接拿一个普通字符串 "张三",或者拿一个数字 1001,一键转换成一个 User 对象:
Rust
struct User {
id: u32,
username: String,
}
// 1. 教 User 如何从一个【&str 字符串】转换而来
impl From<&str> for User {
fn from(name: &str) -> Self {
User {
id: 1, // 默认给个 1
username: String::from(name),
}
}
}
// 2. 还可以再教 User 如何从一个【u32 数字】转换而来!(支持多种原料转换)
impl From<u32> for User {
fn from(id: u32) -> Self {
User {
id,
username: String::from("匿名用户"),
}
}
}
fn main() {
// 见证奇迹:现在你的 User 也能用 ::from() 了!
// 编译器会自动根据你传进去的是字符串还是数字,挑选对应的 from 方法!
let u1 = User::from("张三");
let u2 = User::from(9527);
println!("u1: id={}, name={}", u1.id, u1.username); // id=1, name=张三
println!("u2: id={}, name={}", u2.id, u2.username); // id=9527, name=匿名用户
}
三、 买一送一的孪生兄弟:只要写了 From,白送 .into()!
这是 Rust 最让人大呼过瘾的"买一送一"机制!
标准库规定:只要你给类型 B 实现了 From<A>,编译器就会自动免费给类型 A 赠送一个 .into() 方法(Into Trait)!
它们俩是一体两面,只是主语和宾语反过来了:
-
B::from(a) :站在终点 B 的角度 ------ "把原料 a 吸进来,造出 B"。
-
a.into() :站在原料 a 的角度 ------ "我摇身一变,直接变成 B"!
用刚才的 User 举例,你一行多余的代码都不用写,天然就可以直接用 .into():
Rust
fn main() {
// 写法 1:用 From(不需要在左边写类型,因为右边写明了 User::)
let u1 = User::from("张三");
// 写法 2:用白送的 .into()!(只要左边标明了类型 : User,右边直接 .into() 自动变身!)
let u2: User = "李四".into();
let u3: User = 8888.into(); // 数字 8888 直接摇身一变成为 User 对象!
}
什么时候 .into() 最爽?(传函数参数时!)
当一个函数要求传入 User(或者要求传入 String)时,你连变量类型都不用标,直接甩一个 .into() 过去,编译器自动根据函数参数类型帮你转换:
Rust
fn register(user: User) {
println!("欢迎新用户: {}", user.username);
}
fn main() {
// 函数要的是 User,我手里只有 "王五",直接 .into() 一键变过去!
register("王五".into());
register(10086.into());
}
(同理,let s: String = "hello".into(); 和 let s = String::from("hello"); 是 100% 等价的!)
总结
-
From<T> 是什么?
它是 Rust 官方统一的类型转换接口,定义了怎么从 T 制造出当前类型 Self。
-
为什么 Rust 全世界都在用 From / Into?
因为 Rust 为了安全,严禁任何隐式自动类型转换(连 i16 小整数都不能自动变成 i32 大整数,防止像 JS 那样出现 "1" + 1 = "11" 的诡异 Bug)。为了兼顾安全与方便,Rust 统一规定大家都通过 From::from() 和 .into() 这扇正门来进行显式类型转换。
父特征 / 前置特征约束(Supertrait)
这个语法叫做 "父特征 / 前置特征约束(Supertrait)"。
如果你熟悉 Java 或 TypeScript,它就相当于接口继承:interface B extends A!
⚠️ 重要区分 :前面我们说 Rust 砍掉了继承,指的是存数据的结构体(struct)之间不能搞继承 ;但纯定义行为的接口(trait)之间,是可以用冒号 : 搞"前置条件捆绑"的!
一、 一句话理解 trait B: A 的含义
当你写下:
Rust
trait B: A {
// ...
}
翻译成人话就是一条霸王条款:
"任何结构体要想实现我这个高级接口 B,必须【先实现基础接口 A】!如果没实现 A,连实现 B 的资格都没有!"
二、 看一个直观的代码例子
假设我们有两个接口:基础接口 Animal(动物) ,和进阶接口 GuideDog(导盲犬) 。
显然,想成为一只"导盲犬",前提是它首先得是个"动物":
Rust
// 1. 基础接口:动物(要求必须能说出自己的名字)
trait Animal {
fn name(&self) -> String;
}
// 2. 进阶接口:导盲犬(注意后面的 : Animal !)
// 规定:想实现 GuideDog,必须先实现 Animal!
trait GuideDog: Animal {
fn guide(&self) {
// 💡 爽点在这里:
// 因为编译器 100% 确定它一定实现了 Animal,
// 所以在 GuideDog 内部,可以直接调用 Animal 接口里的 self.name() 方法!
println!("导盲犬【{}】正在安全为您带路...", self.name());
}
}
现在我们定义一个结构体 Dog(金毛犬)来使用它:
Rust
struct Dog;
// ❌ 如果你不写下面这段 impl Animal,直接去写 impl GuideDog,编译器会当场报错:
// "Dog 没有实现 Animal,没资格当 GuideDog!"
// ✅ 第一步:必须先把前置条件 Animal 实现了
impl Animal for Dog {
fn name(&self) -> String {
String::from("大黄")
}
}
// ✅ 第二步:满足了前置条件后,才能实现 GuideDog
impl GuideDog for Dog {}
fn main() {
let my_dog = Dog;
my_dog.guide(); // 输出:导盲犬【大黄】正在安全为您带路...
}
三、 如果有多个前置条件怎么办?(用 + 号连接)
就像之前泛型里写的 T: PartialOrd + Copy 一样,如果一个超级接口要求你同时具备好几项基础能力,直接用 + 号 串起来即可:
Rust
// 想成为"超级英雄(SuperHero)",必须同时实现:会飞(Fly)+ 会格斗(Fight)
trait SuperHero: Fly + Fight {
fn save_world(&self);
}
四、 彩蛋:你今天早就见过标准库里的这种写法了!
其实在你前面学的知识里,Rust 标准库内部到处都是这种 trait B: A 的写法:
-
为什么能比大小(PartialOrd)就一定能比相等(==)?
因为标准库里你刚学过的 PartialOrd 源码第一行就是:
Rust
pub trait PartialOrd: PartialEq { ... }意思是:"想支持比大小(>、<),必须先支持比相等(==)!"
-
为什么支持自动复制(Copy)的类型一定能手动 .clone()?
因为标准库里的 Copy 源码就是一行空壳:
Rust
pub trait Copy: Clone { }意思是:"想拥有栈上自动复制(Copy)的特权,必须先实现 .clone() 接口!"
总结
-
fn foo<T: A>()(函数里的冒号):要求传进来的类型参数 T 必须实现了接口 A。
-
trait B: A (接口定义上的冒号):要求任何想实现接口 B 的人,必须先实现接口 A,并且在 B 的内部可以直接调用 A 的方法。