Rust 面向对象介绍(AI回答)

如果用一句话来评价 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 的思维转换

  1. 想写一个类(Class)

    复制代码
    →→

    写一个 struct (放字段)+ 一个 impl (放方法)。

  2. 想写构造函数(new)

    复制代码
    →→

    在 impl 里写 pub fn new() -> Self 。

  3. 想写接口/多态(Interface)

    复制代码
    →→

    写一个 trait ,然后用 impl Trait名 for 结构体名 。

  4. 想复用代码(替代 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% 等价的!)


总结

  1. From<T> 是什么?

    它是 Rust 官方统一的类型转换接口,定义了怎么从 T 制造出当前类型 Self。

  2. 为什么 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 的写法:

  1. 为什么能比大小(PartialOrd)就一定能比相等(==)?

    因为标准库里你刚学过的 PartialOrd 源码第一行就是:

    Rust

    复制代码
    pub trait PartialOrd: PartialEq { ... }

    意思是:"想支持比大小(>、<),必须先支持比相等(==)!"

  2. 为什么支持自动复制(Copy)的类型一定能手动 .clone()?

    因为标准库里的 Copy 源码就是一行空壳:

    Rust

    复制代码
    pub trait Copy: Clone { }

    意思是:"想拥有栈上自动复制(Copy)的特权,必须先实现 .clone() 接口!"


总结

  • fn foo<T: A>()(函数里的冒号):要求传进来的类型参数 T 必须实现了接口 A。

  • trait B: A (接口定义上的冒号):要求任何想实现接口 B 的人,必须先实现接口 A,并且在 B 的内部可以直接调用 A 的方法。

相关推荐
duansamve19 分钟前
Flask中视图函数接收请求数据和返回响应实例
python·flask
zh_xuan30 分钟前
kotlin常见面试题
开发语言·kotlin·面试题
用户298698530141 小时前
用 Python 自动化把 JavaScript 代码转换为 Word 文档
javascript·python·api
用户019027581611 小时前
AlphaFeed 标的池(universes)全解:CN_Stock / CN_ETF 与权限边界
python
青少儿编程课堂2 小时前
蓝桥杯青少组图形化 + 全国青少年信息素养大赛 + 电子学会Scratch考级:校园连连看解析
c++·python·算法·bfs·信息学竞赛
用户0332126663672 小时前
如何使用 Python 将 JSON 转换为 Excel
python·excel
小小张说故事2 小时前
Python 的切片到底切了什么:拆开 slice 对象、负步长和 __getitem__
python
草莓熊Lotso2 小时前
【C++ 基于微服务的即时通讯系统】文件存储子服务实战:UUID生成、文件IO封装与全接口单元测试
开发语言·网络·数据库·c++·微服务·架构·单元测试
huaqianzkh3 小时前
WinForm C#:循环修改控件UI闪烁卡顿?SuspendLayout / ResumeLayout 与 BeginUpdate 优化
开发语言·ui·c#