第 5 章:函数与方法——从 `class` 到 `struct` 和 `impl`

到目前为止,我们已经掌握了 Rust 的变量、数据类型和控制流。现在,我们将进入一个对 Java 开发者而言至关重要,也可能感到最“颠覆”的领域:代码的组织结构。

在 Java 的世界里,class(类)是我们思想的中心。它将数据(字段)和行为(方法)封装在一起,构成了我们所理解的“对象”。我们理所当然地认为,这是组织代码的唯一“正确”方式。

那么,当你来到 Rust 的世界,你脑海中第一个问题一定是:Rust 的 class 在哪里?

答案是:Rust 没有 class

请先不要惊慌。Rust 当然有能力创建和组织复杂的抽象,但它采用了另一种不同的、甚至可以说是更灵活的方式。这一章,我们将通过 structimpl 这对“黄金搭档”,来为你重塑“对象”的概念。


struct:数据的容器

首先,我们来认识 struct(结构体)。你可以把它直接类比为 一个只有字段(fields),没有方法(methods)的 Java 类。它就像一个纯粹的 POJO (Plain Old Java Object) 或 Java 14+ 中的 Record,其唯一职责就是定义数据的形状

// 定义一个 User 结构体,用来存放用户数据
struct User {
    active: bool,
    username: String,
    email: String,
    sign_in_count: u64,
}

fn main() {
    // 创建一个 User 结构体的实例
    // 注意,我们必须为每个字段提供一个值
    let mut user1 = User {
        email: String::from("[email protected]"),
        username: String::from("someusername123"),
        active: true,
        sign_in_count: 1,
    };

    // 使用点号 . 来访问字段,和 Java 一样
    user1.email = String::from("[email protected]");

    println!("User email is: {}", user1.email);
}

语法糖(Field Init Shorthand): 如果你的函数参数或局部变量名与 struct 的字段名完全相同,Rust 提供了一个方便的简写语法:

fn build_user(email: String, username: String) -> User {
    User {
        email, // 等同于 email: email,
        username, // 等同于 username: username,
        active: true,
        sign_in_count: 1,
    }
}

这让创建实例的代码变得更加简洁。

到这里,struct 看起来就像一个简单的数据容器。那么,行为(方法)在哪里定义呢?


impl:赋予数据行为

这就是 impl(implementation,实现)大显身手的地方。impl 块专门用来为 struct(或 enumtrait定义关联的行为

思维转变的核心:

  • Java class = 数据(字段) + 行为(方法)的紧密耦合。
  • Rust = struct(定义数据) + impl(实现行为)的明确分离。

让我们为一个 Rectangle 结构体添加一个计算面积的方法:

// 1. 定义数据结构
#[derive(Debug)] // 这个注解让我们可以方便地打印 Rectangle 实例
struct Rectangle {
    width: u32,
    height: u32,
}

// 2. 在 impl 块中为 Rectangle 实现行为
impl Rectangle {
    // 这是一个方法(method)
    fn area(&self) -> u32 {
        self.width * self.height
    }
}

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };

    // 使用 . 语法调用方法,和 Java 一样
    println!(
        "The area of the rectangle {:?} is {} square pixels.",
        rect1,
        rect1.area() // 调用 area 方法
    );
}

&self:Rust 版本的 this

在上面的 area 方法中,你一定注意到了这个奇特的参数 &self。这正是理解 Rust 方法的关键。

self&self&mut self 是 Rust 中 this 关键字的“变体”,但它们更明确、更安全。

  • &self (不可变借用):这是最常见的。它表示该方法借用了这个结构体实例,但不能修改它。这相当于 Java 中一个不会改变任何对象字段的 getter 或普通方法。area 方法只需要读取 widthheight,所以使用 &self

  • &mut self (可变借用):表示该方法可变地借用了实例,因此可以修改实例的字段。这相当于 Java 中的 setter 或任何会改变对象状态的方法。

    impl Rectangle {
        fn scale(&mut self, factor: u32) {
            self.width *= factor;
            self.height *= factor;
        }
    }
    // 调用时需要一个可变的实例
    // let mut rect1 = ...;
    // rect1.scale(2);
    
  • self (获取所有权):这种形式比较少见。它表示该方法会**完全“拥有”**这个实例。一旦调用完成,原来的实例变量将失效,不能再被使用。这在 Java 中没有直接对应,但可以类比于某些 Builder 模式的 build() 方法,调用后 builder 本身就没用了。Rust 在编译期就强制执行这种失效。

思维转变: Rust 将 Java 中隐式的 this 变得明确化。通过 &self&mut self,你(和编译器)在方法签名中就能一眼看出这个方法是否会改变对象的状态,这极大地增强了代码的可读性和安全性。


关联函数:static 方法的替代品

那么,Java 中的 static 方法在 Rust 中如何表示呢?比如我们常用的工厂方法或构造函数。

答案是关联函数(Associated Functions)

impl 块中,任何不以 self 作为第一个参数的函数,都是关联函数。它们仍然与这个 struct 相关联,但你不需要一个实例来调用它。

最常见的关联函数就是名为 new 的构造函数:

impl Rectangle {
    // 这是一个关联函数,因为它没有 self 参数
    // 它通常被用作构造器
    fn new(width: u32, height: u32) -> Self {
        Self { width, height }
    }
}

fn main() {
    // 使用 :: 语法来调用关联函数
    let rect2 = Rectangle::new(10, 20);
}

注意这里的 Self 是一个类型别名,代表 impl 块所针对的类型,即 Rectangle。使用 :: 语法来调用关联函数,这清晰地表明了该函数属于 Rectangle 的命名空间,等同于 Java 中的 Rectangle.new(...)


本章小结:Java 与 Rust 的映射

经过本章的学习,我们可以画出这样一张清晰的对应图:

Java 概念 Rust 概念 描述
class struct + impl 数据与行为分离,通过组合实现
字段 / 成员变量 struct 字段 存储数据的容器
实例方法 (this) 方法 (&self, &mut self) 需要实例才能调用,操作实例数据
static 方法 / 构造器 关联函数 无需实例,通过 :: 调用

我们没有失去任何东西!我们只是用一种更明确、更模块化的方式来组织代码。Rust 这种“数据和行为分离”的设计哲学,鼓励我们更多地使用组合而非继承,这通常被认为是更健壮的软件设计原则。当我们未来学习到 Rust 的“接口”——Trait 时,你会更深刻地体会到这种设计的强大威力。