到目前为止,我们已经掌握了 Rust 的变量、数据类型和控制流。现在,我们将进入一个对 Java 开发者而言至关重要,也可能感到最“颠覆”的领域:代码的组织结构。
在 Java 的世界里,class
(类)是我们思想的中心。它将数据(字段)和行为(方法)封装在一起,构成了我们所理解的“对象”。我们理所当然地认为,这是组织代码的唯一“正确”方式。
那么,当你来到 Rust 的世界,你脑海中第一个问题一定是:Rust 的 class
在哪里?
答案是:Rust 没有 class
。
请先不要惊慌。Rust 当然有能力创建和组织复杂的抽象,但它采用了另一种不同的、甚至可以说是更灵活的方式。这一章,我们将通过 struct
和 impl
这对“黄金搭档”,来为你重塑“对象”的概念。
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
(或 enum
、trait
)定义关联的行为。
思维转变的核心:
- 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
方法只需要读取width
和height
,所以使用&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
时,你会更深刻地体会到这种设计的强大威力。