跳至主要內容

Rust基础(二)

Mr.Lexon大约 15 分钟rust

Rust基础(二)

在前面一章我们介绍了什么是OOPRust思想上的区别,以及Rust的一些基础语法和核心思想。 本章我们围绕OOPObjectInterface,RustStructTrait,介绍他们的基础语法以及核心思想,并且对其进行对比以及优势比较。

OOP

对于OOP来说,ObjectInterface几乎熟悉得不能再熟悉了,几乎所有的编程设计都是围绕ObjectOOP的三大基本原则:

  1. 封装
  2. 多态
  3. 继承 这三大原则几乎贯穿了整个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()),会根据对象的实际类型(DogCat)执行不同的行为(bark()meow())。

目的

提供灵活性和可扩展性。你可以编写出能处理多种不同类型对象的通用代码,而无需在代码中写一堆 if-elseswitch-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 的核心优势:

  1. OOP 的核心思想是将现实世界中的实体(如“用户”、“订单”、“汽车”)映射为程序中的类。这种直观的对应关系使得业务逻辑的抽象和建模过程更加自然,对于初学者和业务分析人员来说都非常友好。
  2. 通过封装,类将数据和操作数据的行为捆绑在一起,并向外界隐藏其复杂的内部实现。这使得类就像一个个独立的“黑箱”模块,提高了代码的可维护性。只要公共接口不变,你就可以安全地修改模块内部,而不用担心会影响到其他部分。
  3. 继承机制允许子类直接复用父类的代码,避免了重复劳动。
  4. 多态机制则提供了强大的扩展能力。你可以编写面向通用接口(父类或接口)的通用代码,当系统需要增加新功能时,只需添加一个新的子类实现该接口即可,原有代码无需改动。

OOP 的内在缺陷与挑战:

  1. 继承带来的问题
    • 脆弱的基类问题:在深层次的继承体系中,对顶层父类的一个微小改动,都可能引发所有子类的连锁崩溃,使得基类的维护变得异常困难和危险。
    • “大猩猩/香蕉”问题:这是一个经典比喻,意指你可能只想复用子类的一小部分功能(香蕉),却因为继承关系,被迫引入了整个庞大而复杂的父类继承体系(大猩猩和整片丛林)。
    • 多重继承的复杂性(菱形问题):一些语言支持多重继承,但这会带来“菱形问题”,即一个类从两个或更多间接共享同一个基类的父类继承时,会导致成员访问的歧义和混乱。
    • 紧耦合:继承本质上是一种非常强的耦合关系,子类的实现与父类的实现细节紧密绑定。
  2. 状态管理的复杂性
    • OOP 的核心是围绕可变的对象。在大型应用中,一个对象的状态可能在程序的不同地方被共享和修改,这使得追踪状态变化变得极其困难,尤其是在并发环境中,很容易导致数据竞争和各种难以调试的 bug。
  3. 封装可能被破坏
    • 理论上封装可以保护数据,但在实践中,通过反射(Reflection)等语言特性可以绕过访问限制。更常见的是,不良的设计(如为每个私有字段都提供公共的 getter 和 setter)会使得封装形同虚设,对象的内部状态实际上完全暴露给了外部。
    • 并且在实践中,封装被破坏是常有的事情,因为在设计阶段,属性设计几乎没有通过详细论证(需求模糊几乎是常有的事情)。所以封装往往在实践中形同虚设。 那么OOP这些特点以及劣势,能否在Rust得到发扬和体现呢?我们来看看RustStructTrait

RustStructTrait

在 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, 
}

通过组合,我们优雅地解决了数据复用的问题。StudentTeacher 都是独立的类型,它们通过包含一个 Person 结构体来共享通用字段。 但是,共享的行为(比如 displayInfo 方法)和多态又该如何实现呢?这就需要用到 Rust 最强大的抽象工具——Trait

Trait

我们先介绍一下trait与java的interface之间有什么区别于优势。

相似之处

从抽象层次来看,interfacetrait 的功能是一致的:定义共享的行为

  1. 行为抽象与契约 两者都允许你定义一组方法签名,任何实现了它们的类型都必须提供这些方法的具体实现。它们都扮演着“契约”的角色。
  2. 实现多态 两者都是实现多态的核心工具,允许你编写通用的代码来处理实现了同一接口/trait 的不同类型。
  3. 默认方法 (Default Methods) 从 Java 8 开始,interface 可以有 default 方法。这与 Rust trait 可以为方法提供默认实现的功能非常相似,都允许在不破坏现有实现的情况下扩展功能。

核心区别

尽管目标相似,但 trait 在设计和能力上比 interface 更强大、更灵活。

  1. 彻底的数据与行为分离
    • Java 的 interface 不能有实例字段,但可以有静态常量。
    • Rust 的 trait 完全不能包含任何数据字段。它只关心行为。这强制贯彻了 Rust 中“数据(struct/enum)与行为(trait)相分离”的设计哲学。
  2. 实现的灵活性(孤儿规则)
    • 在 Java 中,一个类要实现一个接口,必须在自己的源代码中明确声明 implements MyInterface。你无法让一个来自第三方库的类去实现你新写的接口,除非你修改它的源码。
    • 在 Rust 中,impl MyTrait for SomeType 代码块可以放在任何地方,只要满足孤儿规则你正在实现的 trait 或者你正在为其实现 traittype,至少有一个是在当前包(crate)中定义的。
    • 可以为自己的类型 MyStruct 实现标准库的 Display trait:impl std::fmt::Display for MyStruct { ... }
    • 也可以为自己的 MyTrait 给标准库的 Vec<i32> 类型实现功能:impl MyTrait for Vec<i32> { ... }
  3. 静态分派 vs. 动态分派
    • Java 的接口多态几乎总是通过动态分派实现的,有轻微的性能开销。
    • Rust 的 trait 同时支持两种方式,但是通常都是静态分发。
  4. 关联类型
    • trait 可以定义关联类型,让实现者来指定具体的类型。 总结来说,Java 的 interface 主要是一种实现运行时多态和 API 契约的工具。而 Rust 的 trait 不仅涵盖了这些功能,它更是一种贯穿整个语言的、更强大的核心抽象机制。它与泛型结合实现了零成本的静态多态,通过孤儿规则提供了强大的扩展性,并与类型系统深度集成以保证安全。

如何使用

trait的定义:

// 定义一个契约:任何“可展示信息”的东西都必须有 display_info 方法 
pub trait Displayable { 
	fn display_info(&self); 
}

现在,我们为我们的 StudentTeacher 类型实现这个契约。注意,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 选择的路径却截然不同。Structimpl 的分离,教会了我们将数据行为解耦。Struct 回归其作为纯粹数据载体的本质,而 impl 块则让我们能灵活地、有组织地为这些数据附加能力。Trait 作为行为契约,取代了 class 的继承。它允许任何类型在不被锁定于某个继承体系的情况下,选择实现某些共享行为,提供了无与伦比的灵活性。 最终,我们通过组合优于继承的实例,将这些思想融会贯通。我们不再构建一个僵化的“is-a”(是一个)的层次结构,而是创建了一个灵活的“has-a”(有一个)和“can-do”(能做什么)的抽象系统。 总而言之,从 OOP 过渡到 Rust,其核心是一次深刻的思维转变:从 关注对象,转向 关注数据的结构 (Structs) 与其能实现的行为 (Traits)。 掌握这种思想,是编写出优雅且可维护的 Rust 代码的关键。在接下来的章节中,我们将继续探讨 Rust 的另一个基石——所有权系统。

上次编辑于:
贡献者: lexon