本篇导读
前面几篇我们一直在和零散的变量、字符串、元组打交道。但真实程序里,数据往往是成组出现的:一个“用户”有姓名、年龄、邮箱;一个“矩形”有宽和高;一本书有书名、作者、页数。把这些本就相关的数据拆散成一个个独立变量,既不直观也容易出错。
今天的主角——结构体(struct)——就是 Rust 用来把相关数据打包成一个有意义整体的工具,它类似其他语言中“对象的数据部分”“记录(record)”。
本篇你将学会:定义与实例化具名结构体、字段简写与结构体更新语法、元组结构体与单元结构体、用 impl 块定义方法与关联函数,以及一个贯穿全篇的“矩形面积”案例。
一、定义并实例化结构体
1. 定义结构体
用 struct 关键字给结构体命名,并在花括号中列出每个 字段(field) 的名字和类型:
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}
2. 创建实例
定义只是“蓝图”,使用时要创建具体的 实例,为每个字段赋值:
fn main() {
let mut user1 = User {
active: true,
username: String::from("小明"),
email: String::from("xiaoming@example.com"),
sign_in_count: 1,
};
// 用点号访问字段
println!("用户名:{}", user1.username);
// 修改字段(要求整个实例是 mut 的)
user1.email = String::from("new@example.com");
println!("新邮箱:{}", user1.email);
}
两个要点:
- 访问字段用
实例.字段名;
- Rust 不允许只把个别字段标记为可变——要么整个实例是
mut,要么都不可变。
3. 字段初始化简写
当函数参数名与字段名相同时,可以使用 字段简写(field init shorthand),省去重复书写:
fn build_user(email: String, username: String) -> User {
User {
active: true,
username, // 等价于 username: username
email, // 等价于 email: email
sign_in_count: 1,
}
}
4. 结构体更新语法:从旧实例创建新实例
想基于一个已有实例、只改少数字段来创建新实例,可以用 .. 语法:
let user2 = User {
email: String::from("another@example.com"),
..user1 // 其余字段从 user1 取值
};
注意:..user1 必须放在最后。这里有一个与所有权相关的重要细节——它会“移动”那些没有被显式给出的字段。因为 username 是 String(堆类型),它的所有权从 user1 移动到了 user2,所以更新后 user1 中的 username 不能再用;但 active、sign_in_count 是 Copy 类型,不受影响。
二、元组结构体与单元结构体
并非所有结构体都需要给字段命名,Rust 还提供了两种变体。
1. 元组结构体(Tuple Structs)
元组结构体有结构体的名字,但字段没有名字,只有类型:
struct Color(i32, i32, i32);
struct Point(i32, i32, i32);
fn main() {
let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);
println!("红色分量:{}", black.0);
}
它特别适合“我想给一个元组起个专门的类型名”的场景。注意 Color 和 Point 虽然字段结构相同,但它们是 不同的类型,不能互相赋值——这正是元组结构体的价值:用类型区分含义相同但语义不同的数据(newtype 模式,第 21 篇还会见到)。
2. 单元结构体(Unit-like Structs)
没有任何字段的结构体:
struct AlwaysEqual;
fn main() {
let subject = AlwaysEqual;
}
它只占一个类型名、不占数据空间。当你需要为某个类型实现 trait(行为),但类型本身不需要存储任何数据时会用到,在第 11 篇之后会更有体会。
三、结构体的数据所有权
你可能注意到 User 中我们用的是 String 而不是 &str。这是有意的:结构体默认拥有它字段的数据,只要结构体有效,字段数据就有效,这种设计最简单直接。
结构体当然也可以存放引用,但引用意味着结构体“借用”了别处的数据,为了保证在结构体存活期间被引用的数据一直有效,就必须为引用标注生命周期。这是第 12 篇的内容,现在我们先让结构体拥有全部数据。
四、为结构体定义方法
光有数据还不够,我们还希望数据“能做事”。在 Rust 中,通过 impl(implementation 的缩写)块为结构体定义 方法。
1. 矩形面积:从元组到结构体
假设要计算矩形面积。用结构体表达矩形非常自然:
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
// &self 是 self: &Self 的简写,表示方法借用当前实例(只读)
fn area(&self) -> u32 {
self.width * self.height
}
}
fn main() {
let rect = Rectangle { width: 30, height: 50 };
println!("矩形面积:{} 平方像素", rect.area());
}
2. 读懂 &self
- 方法的第一个参数通常是
self,代表“调用这个方法的那个实例”;
&self 是 self: &Self 的简写,表示 只读地借用 实例。Self 是 impl 块所针对类型的别名(这里就是 Rectangle);
- 如果方法需要修改实例,就写
&mut self;如果方法要拿走实例的所有权,就直接写 self。
方法与函数的区别仅在于:方法定义在 impl 块中、第一个参数是实例,并通过 实例.方法名() 调用。
3. 方法可以和字段同名
Rust 允许方法与字段同名,常用于实现“访问器(getter)”:
impl Rectangle {
fn width(&self) -> bool {
self.width > 0
}
}
// rect.width 带括号是方法,不带括号是字段
4. 带更多参数的方法
比如判断一个矩形能否完全容纳另一个矩形:
impl Rectangle {
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
}
fn main() {
let rect1 = Rectangle { width: 30, height: 50 };
let rect2 = Rectangle { width: 10, height: 40 };
let rect3 = Rectangle { width: 60, height: 45 };
println!("rect1 能容纳 rect2 吗?{}", rect1.can_hold(&rect2)); // true
println!("rect1 能容纳 rect3 吗?{}", rect1.can_hold(&rect3)); // false
}
5. 关联函数:不以 self 为参数
impl 块中还可以定义 不以 self 为第一个参数 的函数,它们叫 关联函数(associated functions),也就是我们早就用过的 String::new()。它们不针对某个实例,而是属于类型本身,用 类型名::函数名() 调用。
最典型的用途是构造函数,惯例命名为 new:
impl Rectangle {
fn square(size: u32) -> Self {
Self {
width: size,
height: size,
}
}
}
fn main() {
let sq = Rectangle::square(20); // 注意是 :: 而不是 .
println!("正方形面积:{}", sq.area());
}
一个类型可以有多个 impl 块(Rust 允许),但通常把相关方法组织在一个块里。
五、一个有用的小技巧:打印结构体调试信息
调试时你可能想直接打印整个结构体看看,但下面的代码默认编译不过:
println!("rect = {}", rect); // Rectangle 没有实现 Display
Rust 不知道该用什么“人类友好格式”显示自定义结构体。但它提供了专门的 调试格式,只需在结构体定义上方加上一个 派生属性:
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rect = Rectangle { width: 30, height: 50 };
println!("rect = {rect:?}"); // 单行调试格式
println!("rect = {rect:#?}"); // 多行美化格式
}
#[derive(Debug)] 让编译器自动生成调试打印所需的代码;
{:?} 是调试占位符,{:#?} 是美化版。
输出:
rect = Rectangle { width: 30, height: 50 }
rect = Rectangle {
width: 30,
height: 50,
}
此外还可以用 dbg! 宏获取一个表达式的 文件、行号和值(它会拿走并归还所有权,打印到标准错误流):
let scale = 2;
let rect = Rectangle {
width: dbg!(30 * scale), // 打印:[src/main_1:..] 30 * scale = 60
height: 50,
};
dbg!(&rect);
derive(派生)是 Rust 极其常用的机制,编译器能为若干内置 trait 自动生成实现,第 11、24 篇会深入。
六、综合示例:用结构体建模一个银行账户
把今天的内容串成一个稍完整的例子:
#[derive(Debug)]
struct Account {
owner: String,
balance: f64,
}
impl Account {
// 关联函数:开户
fn new(owner: &str, initial: f64) -> Self {
Self {
owner: owner.to_string(),
balance: initial,
}
}
// 存款(需要 &mut self)
fn deposit(&mut self, amount: f64) {
if amount > 0.0 {
self.balance += amount;
}
}
// 取款,返回是否成功
fn withdraw(&mut self, amount: f64) -> bool {
if amount > 0.0 && amount <= self.balance {
self.balance -= amount;
true
} else {
false
}
}
// 只读查询
fn balance(&self) -> f64 {
self.balance
}
}
fn main() {
let mut acc = Account::new("小明", 100.0);
acc.deposit(50.0);
if acc.withdraw(30.0) {
println!("取款成功");
}
if !acc.withdraw(1000.0) {
println!("余额不足,取款失败");
}
println!("账户信息:{acc:#?}");
println!("当前余额:{}", acc.balance());
}
这个例子完整展示了:关联函数做构造器、&mut self 做修改、&self 做查询、返回值表达业务结果、derive(Debug) 打印。这就是 Rust 中“数据 + 行为”最基本的组织方式。
七、本篇小结
- 结构体 把若干相关字段打包成有意义的整体,用
struct 定义、点号访问字段,可变性作用于整个实例;
- 字段名与变量名相同时可用 字段简写;用
..旧实例 可基于旧实例更新,注意其中堆字段会发生移动;
- 元组结构体 给元组命名、用于区分语义;单元结构体 无字段、用于实现行为;
- 结构体默认拥有字段数据;存放引用需要生命周期(第 12 篇);
- 用
impl 块 定义方法:&self 只读、&mut self 修改、self 获取所有权;
- 关联函数 没有
self,用 类型::函数() 调用,常用于名为 new 的构造器;
#[derive(Debug)] 配合 {:?}、{:#?} 和 dbg! 可方便地调试结构体。
八、动手练习
- 给
Rectangle 增加一个方法 perimeter(&self) -> u32 计算周长;
- 定义一个
Circle 结构体(半径 f64),实现 area 方法,并写一个关联函数 diameter(&self) -> f64(注意:它其实是方法);
- 用元组结构体定义
Kilometers(i32) 和 Miles(i32),验证不能把二者直接相加或赋值;
- 扩展银行账户例子,增加一个
transfer(from: &mut Account, to: &mut Account, amount: f64) -> bool 关联函数实现转账;
- (选做)定义
struct Point { x: f64, y: f64 },实现方法 distance(&self, other: &Point) -> f64 计算两点距离(公式:√((x1−x2)²+(y1−y2)²),平方根方法是 (x).sqrt())。
下篇预告:第 07 篇|枚举与模式匹配
下一篇我们学习 Rust 类型系统中另一个极其强大的特性——枚举(enum),尤其是标准库中无处不在的 Option<T>(表示“可能有值、可能没有”)和 Result<T, E>。
Rust 的枚举不是 C 语言那种简单的整数代号,而是每个变体都可以携带不同的数据,称为代数数据类型。配合 match 穷尽匹配和 if let 语法,它能从根本上消灭“空指针异常”。这是 Rust 优雅与安全的重要来源,下篇见。