Rust基础(二)
Rust基础(二)
在前面一章我们介绍了什么是OOP和Rust思想上的区别,以及Rust的一些基础语法和核心思想。 本章我们围绕OOP的Object和Interface,Rust的Struct和Trait,介绍他们的基础语法以及核心思想,并且对其进行对比以及优势比较。
OOP
对于OOP来说,Object和Interface几乎熟悉得不能再熟悉了,几乎所有的编程设计都是围绕Object,OOP的三大基本原则:
- 封装
- 多态
- 继承 这三大原则几乎贯穿了整个
OOP编程设计,以防读者忘记了这些概念,我们首先回顾一下这三大原则:
封装
定义
封装是指将 数据(属性) 和操作这些 数据的方法(行为) 捆绑到一个独立的单元中,这个单元就是“对象”。核心思想是信息隐藏。对象的内部状态被设为私有,外部世界只能通过对象提供的公共接口来访问和修改数据。
目的
保护对象的数据不被随意篡改,确保其始终处于一个有效的、一致的状态。
示例代码(JAVA):
public class Person {
public String name;
private int birthYear;//这里的year就被封装进去了,所以只能通过成员方法访问
//成员方法
public int getBirthYear(){
return this.birthYear;
}
public Person(String name, int birthYear) {
this.name = name;
this.birthYear = birthYear;
}
}
public class main {
public static void main(String[] args){
Person p = new Person();
// p.year 无法访问
System.out.println(p.name);//可以访问
System.out.println(p.getBirthYear());//通过成员方法访问
}
}
继承
定义
继承是一种机制,允许一个类(子类)获取另一个类(父类)的属性和方法。它建立了一种上下传递(单向的)的关系,子类可以使用父类的protect标注或者是public标注的属性或者是方法 。例如,Dog is an Animal。
目的
实现代码复用,并建立一个清晰的类型层次结构。子类可以重用父类的代码,也可以重写或扩展父类的行为。
代码示例(JAVA):
public class Person {
protected String name;
private int birthYear;
public Person(String name, int birthYear) {
this.name = name;
this.birthYear = birthYear;
}
public int getBirthYear() {
return this.birthYear;
}
public void displayInfo() {
System.out.println("Name: " + this.name);
System.out.println("Birth Year: " + this.birthYear);
}
}
// Student 类继承自 Person 类
public class Student extends Person {
public String studentClass;
public Student(String name, int birthYear, String studentClass) {
super(name, birthYear);
this.studentClass = studentClass;
}
@Override
public void displayInfo() {
super.displayInfo();
System.out.println("Class: " + this.studentClass);
}
}
public class Main {
public static void main(String[] args) {
Student student = new Student("小明", 2005, "高三二班");
// 'name' 是 protected 的,子类实例可以直接访问
System.out.println("学生姓名 (继承自 Person): " + student.name);
// Student 类没有定义 getBirthYear(),但它从 Person 类继承了此方法
System.out.println("出生年份 (调用继承的方法): " + student.getBirthYear());
System.out.println("所在班级 (子类自有属性): " + student.studentClass);
// 程序会执行 Student 类中定义的 displayInfo() 版本
student.displayInfo();
}
}
多态
定义
多态的字面意思是“多种形态”。在OOP中,它指的是父类的引用可以指向其子类的实例,从而允许我们以一种统一的方式处理不同类型的对象。调用同一个方法(如 animal.make_sound()),会根据对象的实际类型(Dog 或 Cat)执行不同的行为(bark() 或 meow())。
目的
提供灵活性和可扩展性。你可以编写出能处理多种不同类型对象的通用代码,而无需在代码中写一堆 if-else 或 switch-case 来判断具体类型。
代码例子(JAVA):
public class Person {
protected String name;
private int birthYear;
public Person(String name, int birthYear) {
this.name = name;
this.birthYear = birthYear;
}
public int getBirthYear() {
return this.birthYear;
}
public void displayInfo() {
System.out.println("Name: " + this.name);
System.out.println("Birth Year: " + this.birthYear);
}
}
// Student 类继承自 Person 类
public class Student extends Person {
public String studentClass;
public Student(String name, int birthYear, String studentClass) {
super(name, birthYear);
this.studentClass = studentClass;
}
@Override
public void displayInfo() {
super.displayInfo();
System.out.println("Class: " + this.studentClass);
}
}
// Teacher 类也继承自 Person 类
public class Teacher extends Person {
// Teacher 特有的字段
public String subject;
public Teacher(String name, int birthYear, String subject) {
// 同样调用父类构造函数
super(name, birthYear);
this.subject = subject;
}
// 重写 displayInfo 方法以展示教师的特定信息
@Override
public void displayInfo() {
super.displayInfo();
System.out.println("Subject: " + this.subject);
}
}
public class Main {
public static void main(String[] args) {
Person student = new Student("小明", 2005, "高三二班");
Person teacher = new Teacher("王老师", 1985, "数学");
Person anotherStudent = new Student("小红", 2006, "高三一班");
// 将这些不同的对象放入一个父类型(Person)的数组中
// 这就是多态的应用:Student 和 Teacher 对象都可以被当作 Person 来对待
Person[] people = {student, teacher, anotherStudent};
// 遍历这个 Person 数组,对每个元素调用同一个方法
for (Person person : people) {
// 关键点:这里的 person 变量是 Person 类型,但程序会在运行时
// 判断其真实类型(是 Student 还是 Teacher),并调用相应类中
// 被重写过的 displayInfo() 方法。
person.displayInfo();
}
}
}
这三个原则几乎涵盖了Object的使用方法和原则,那么我们来看看Interface的语法:
Interface
定义以及目的
接口可以被理解为一个完全抽象的“契约”或“行为规范”,但是主要的使用方法就是对实现接口的类标记方法。 示例代码(JAVA):
// 定义一个“可显示信息”的接口契约
public interface Displayable {
// 任何实现了此接口的类,都必须提供 displayInfo() 方法
void displayInfo();
}
public class Person {
// 字段现在可以直接设为 public,或者通过 public getter 访问
public String name;
public int birthYear;
public Person(String name, int birthYear) {
this.name = name;
this.birthYear = birthYear;
}
}
// Student 类实现了 Displayable 接口
public class Student implements Displayable {
// 它“有一个”Person 对象(组合),而不是“是一个”Person
private Person personInfo;
public String studentClass;
public Student(String name, int birthYear, String studentClass) {
// 创建并持有 Person 对象
this.personInfo = new Person(name, birthYear);
this.studentClass = studentClass;
}
// 必须实现接口中定义的方法
@Override
public void displayInfo() {
// 从包含的 Person 对象中获取信息
System.out.println("Name: " + this.personInfo.name);
System.out.println("Birth Year: " + this.personInfo.birthYear);
System.out.println("Class: " + this.studentClass);
}
}
// Teacher 类也实现了 Displayable 接口
public class Teacher implements Displayable {
private Person personInfo;
public String subject;
public Teacher(String name, int birthYear, String subject) {
this.personInfo = new Person(name, birthYear);
this.subject = subject;
}
// 必须实现接口中定义的方法
@Override
public void displayInfo() {
System.out.println("Name: " + this.personInfo.name);
System.out.println("Birth Year: " + this.personInfo.birthYear);
System.out.println("Subject: " + this.subject);
}
}
public class Main {
public static void main(String[] args) {
//创建实现了 Displayable 接口的不同类的实例
Displayable student = new Student("小明", 2005, "高三二班");
Displayable teacher = new Teacher("王老师", 1985, "数学");
Displayable[] itemsToDisplay = {student, teacher};
for (Displayable item : itemsToDisplay) {
// 程序会在运行时调用其真实类型(Student 或 Teacher)的具体实现
item.displayInfo();
}
}
}
那么通过这三个原则和代码示例,我们不难看出: OOP 的核心优势:
- OOP 的核心思想是将现实世界中的实体(如“用户”、“订单”、“汽车”)映射为程序中的类。这种直观的对应关系使得业务逻辑的抽象和建模过程更加自然,对于初学者和业务分析人员来说都非常友好。
- 通过封装,类将数据和操作数据的行为捆绑在一起,并向外界隐藏其复杂的内部实现。这使得类就像一个个独立的“黑箱”模块,提高了代码的可维护性。只要公共接口不变,你就可以安全地修改模块内部,而不用担心会影响到其他部分。
- 继承机制允许子类直接复用父类的代码,避免了重复劳动。
- 多态机制则提供了强大的扩展能力。你可以编写面向通用接口(父类或接口)的通用代码,当系统需要增加新功能时,只需添加一个新的子类实现该接口即可,原有代码无需改动。
OOP 的内在缺陷与挑战:
- 继承带来的问题
- 脆弱的基类问题:在深层次的继承体系中,对顶层父类的一个微小改动,都可能引发所有子类的连锁崩溃,使得基类的维护变得异常困难和危险。
- “大猩猩/香蕉”问题:这是一个经典比喻,意指你可能只想复用子类的一小部分功能(香蕉),却因为继承关系,被迫引入了整个庞大而复杂的父类继承体系(大猩猩和整片丛林)。
- 多重继承的复杂性(菱形问题):一些语言支持多重继承,但这会带来“菱形问题”,即一个类从两个或更多间接共享同一个基类的父类继承时,会导致成员访问的歧义和混乱。
- 紧耦合:继承本质上是一种非常强的耦合关系,子类的实现与父类的实现细节紧密绑定。
- 状态管理的复杂性
- OOP 的核心是围绕可变的对象。在大型应用中,一个对象的状态可能在程序的不同地方被共享和修改,这使得追踪状态变化变得极其困难,尤其是在并发环境中,很容易导致数据竞争和各种难以调试的 bug。
- 封装可能被破坏
- 理论上封装可以保护数据,但在实践中,通过反射(Reflection)等语言特性可以绕过访问限制。更常见的是,不良的设计(如为每个私有字段都提供公共的 getter 和 setter)会使得封装形同虚设,对象的内部状态实际上完全暴露给了外部。
- 并且在实践中,封装被破坏是常有的事情,因为在设计阶段,属性设计几乎没有通过详细论证(需求模糊几乎是常有的事情)。所以封装往往在实践中形同虚设。 那么
OOP这些特点以及劣势,能否在Rust得到发扬和体现呢?我们来看看Rust的Struct和Trait
Rust的Struct和Trait
在 OOP 中,类是数据和行为的混合体。Rust 的第一步是要求我们将它们分开。我们先来处理数据部分,这就要用到 struct。
Struct
我们在前面一章提到过,Struct更倾向于做一组数据集合,但是不意味着Struct不能实现成员方法,这是一组例子:
// 使用 struct 来定义 Person,它包含了数据字段
// derive(Debug) 让我们能使用 {:?} 格式打印结构体以进行调试
#[derive(Debug)]
pub struct Person {
// `pub` 关键字让这个字段变为公共的,外部可以直接访问
pub name: String,
// 没有 `pub` 关键字的字段默认是私有的
year: i32,
}
// `impl` 块用于为 Person struct 定义关联函数和方法
impl Person {
// 在 Rust 中,通常使用名为 `new` 的关联函数作为构造器
// 它返回一个 Self 的实例,这里的 Self 指代 Person 类型
pub fn new(name: &str, year: i32) -> Self {
Person {
name: name.to_string(),
year: year,
}
}
// 一个公共方法(getter),用于从外部访问私有字段 `year`
// `&self` 表示这个方法借用了当前实例的不可变引用
pub fn get_year(&self) -> i32 {
self.year // Rust 中,表达式作为最后一行业会自动成为返回值
}
}
fn main() {
// 1. 使用 Person::new() "构造器" 来创建一个 Person 实例
let person = Person::new("Alice", 1990);
// 2. 直接访问公共字段 `name`
println!("Person's name is: {}", person.name);
// 3. 通过公共方法 `get_year()` 访问私有字段 `year`
println!("Person's birth year is: {}", person.get_year());
// 使用调试打印来查看整个实例
println!("Debug print of person: {:?}", person);
}
那对于上面的例子,我们在rust可就不能直接使用继承了,Rust 推崇“组合优于继承”的原则。我们不让 Student “成为”一个 Person,而是让 Student 内部“拥有”一个 Person 的数据。这是一种 “有一个 (has-a)” 的关系。
// 包含通用信息的数据块
pub struct Person {
pub name: String,
birth_year: i32,
}
// 学生类型:它“有一个”Person
pub struct Student {
person_data: Person,
pub class: String,
}
// 教师类型:它也“有一个”Person
pub struct Teacher {
person_data: Person,
pub subject: String,
}
通过组合,我们优雅地解决了数据复用的问题。Student 和 Teacher 都是独立的类型,它们通过包含一个 Person 结构体来共享通用字段。 但是,共享的行为(比如 displayInfo 方法)和多态又该如何实现呢?这就需要用到 Rust 最强大的抽象工具——Trait。
Trait
我们先介绍一下trait与java的interface之间有什么区别于优势。
相似之处
从抽象层次来看,interface 和 trait 的功能是一致的:定义共享的行为。
- 行为抽象与契约 两者都允许你定义一组方法签名,任何实现了它们的类型都必须提供这些方法的具体实现。它们都扮演着“契约”的角色。
- 实现多态 两者都是实现多态的核心工具,允许你编写通用的代码来处理实现了同一接口/trait 的不同类型。
- 默认方法 (Default Methods) 从 Java 8 开始,
interface可以有default方法。这与 Rusttrait可以为方法提供默认实现的功能非常相似,都允许在不破坏现有实现的情况下扩展功能。
核心区别
尽管目标相似,但 trait 在设计和能力上比 interface 更强大、更灵活。
- 彻底的数据与行为分离
- Java 的
interface不能有实例字段,但可以有静态常量。 - Rust 的
trait完全不能包含任何数据字段。它只关心行为。这强制贯彻了 Rust 中“数据(struct/enum)与行为(trait)相分离”的设计哲学。
- Java 的
- 实现的灵活性(孤儿规则)
- 在 Java 中,一个类要实现一个接口,必须在自己的源代码中明确声明
implements MyInterface。你无法让一个来自第三方库的类去实现你新写的接口,除非你修改它的源码。 - 在 Rust 中,
impl MyTrait for SomeType代码块可以放在任何地方,只要满足孤儿规则:你正在实现的trait或者你正在为其实现trait的type,至少有一个是在当前包(crate)中定义的。 - 可以为自己的类型
MyStruct实现标准库的Displaytrait:impl std::fmt::Display for MyStruct { ... } - 也可以为自己的
MyTrait给标准库的Vec<i32>类型实现功能:impl MyTrait for Vec<i32> { ... }
- 在 Java 中,一个类要实现一个接口,必须在自己的源代码中明确声明
- 静态分派 vs. 动态分派
- Java 的接口多态几乎总是通过动态分派实现的,有轻微的性能开销。
- Rust 的
trait同时支持两种方式,但是通常都是静态分发。
- 关联类型
trait可以定义关联类型,让实现者来指定具体的类型。 总结来说,Java 的interface主要是一种实现运行时多态和 API 契约的工具。而 Rust 的trait不仅涵盖了这些功能,它更是一种贯穿整个语言的、更强大的核心抽象机制。它与泛型结合实现了零成本的静态多态,通过孤儿规则提供了强大的扩展性,并与类型系统深度集成以保证安全。
如何使用
trait的定义:
// 定义一个契约:任何“可展示信息”的东西都必须有 display_info 方法
pub trait Displayable {
fn display_info(&self);
}
现在,我们为我们的 Student 和 Teacher 类型实现这个契约。注意,Rust 允许我们为同一个 struct 创建多个 impl 块,这有助于我们按逻辑组织代码。
impl Person {
// 构造器
pub fn new(name: &str, year: i32) -> Self {
Person {
name: name.to_string(),
birth_year: year
}
}
}
impl Student {
pub fn new(name: &str, year: i32, class: &str) -> Self {
Student {
person_data: Person::new(name, year),
class: class.to_string(),
}
}
}
// 让 Student 遵守 Displayable 契约
impl Displayable for Student {
fn display_info(&self) {
println!("Name: {}", self.person_data.name);
println!("Birth Year: {}", self.person_data.birth_year);
println!("Class: {}", self.class);
}
}
impl Teacher {
pub fn new(name: &str, year: i32, subject: &str) -> Self {
Teacher {
person_data: Person::new(name, year),
subject: subject.to_string(),
}
}
}
impl Displayable for Teacher {
fn display_info(&self) {
println!("Name: {}", self.person_data.name);
println!("Birth Year: {}", self.person_data.birth_year);
println!("Subject: {}", self.subject);
}
}
至此,我们已经拥有了所有拼图:代表纯粹数据的 struct 和定义共享行为的 trait。最后一步,就是将它们组合起来,实现我们最初想要的多态效果。
最后组合
现在,我们来看看 Rust 如何统一处理这些不同类型但实现了相同 Trait 的对象。我们将使用Trait 对象 (dyn Trait) 来实现动态分派。
// Trait 和 Struct 定义... (省略之前的代码)
fn main() {
// 创建不同类型的实例 let student = Student::new("小明", 2005, "高三二班");
let teacher = Teacher::new("王老师", 1985, "数学");
// 将这些不同类型的对象放入一个 Vec 中
// Vec<Box<dyn Displayable>> 可以持有任何实现了 Displayable trait 的类型的实例。
// 这正是通过 Trait 实现的多态!
let people: Vec<Box<dyn Displayable>> = vec![
Box::new(student),
Box::new(teacher),
];
// 统一调用 display_info() 方法
for person in &people {
person.display_info();
println!("");
}
}
这个 main 函数完美地回应了我们在文章开头提出的问题。我们成功地用 Rust 的工具,以一种完全不同的方式,实现了与 Java 继承版本相同的多态效果。
总结
在本章中,我们踏上了一段从传统面向对象(OOP)到 Rust 独特设计哲学的旅程。我们看到,尽管目标相似——创建模块化、可复用的代码——但 Rust 选择的路径却截然不同。Struct 与 impl 的分离,教会了我们将数据和行为解耦。Struct 回归其作为纯粹数据载体的本质,而 impl 块则让我们能灵活地、有组织地为这些数据附加能力。Trait 作为行为契约,取代了 class 的继承。它允许任何类型在不被锁定于某个继承体系的情况下,选择实现某些共享行为,提供了无与伦比的灵活性。 最终,我们通过组合优于继承的实例,将这些思想融会贯通。我们不再构建一个僵化的“is-a”(是一个)的层次结构,而是创建了一个灵活的“has-a”(有一个)和“can-do”(能做什么)的抽象系统。 总而言之,从 OOP 过渡到 Rust,其核心是一次深刻的思维转变:从 关注对象,转向 关注数据的结构 (Structs) 与其能实现的行为 (Traits)。 掌握这种思想,是编写出优雅且可维护的 Rust 代码的关键。在接下来的章节中,我们将继续探讨 Rust 的另一个基石——所有权系统。