Rust 结构体
结构体(struct)是 Rust 组织数据的核心工具,相当于其他语言的"对象"。但 Rust 没有"类"的概念,而是把数据(struct)和行为(impl 块)分开定义。这一篇我们讲三种结构体、impl 方法、关联函数,这是写任何 Rust 程序的基础。
1. 命名结构体(最常用)
命名结构体有具名字段,是最常用的形式。每个字段有名字和类型:
// 命名结构体:最常用,字段有名字
struct User {
name: String,
age: u32,
active: bool,
}
fn main() {
// 创建实例:每个字段都初始化
let user1 = User {
name: String::from("小明"),
age: 20,
active: true,
};
// 读取字段用 . 操作符
println!("{} {} {}", user1.name, user1.age, user1.active);
// 修改字段:整个实例必须 mut(不能只让某字段可变)
let mut user2 = User {
name: String::from("小红"),
age: 22,
active: true,
};
user2.age = 23; // OK,因为 user2 是 mut
// user1.age = 25; // 编译错误!user1 不可变
// 字段简写:变量名和字段名相同时
let name = String::from("小刚");
let age = 25;
let user3 = User { name, age, active: true }; // 等同 name: name
}几个细节:
- 创建实例:用花括号初始化每个字段。
- 整个实例的可变性:必须声明
mut整个变量,不能只让某字段可变。这是 Rust 简化设计。 - 字段简写:变量名和字段名相同时,可以省略冒号赋值,类似 JS 的对象简写。
2. 结构体更新语法
基于旧实例创建新实例时,可以用 ..old 语法,只改几个字段,其余从旧实例拿:
fn main() {
let user1 = User {
name: String::from("小明"),
age: 20,
active: true,
};
// 结构体更新语法:基于旧实例创建新实例,只改几个字段
let user2 = User {
age: 30,
..user1 // 其余字段从 user1 拿
};
println!("{} {}", user2.name, user2.age); // 小明 30
// 注意:..user1 会【move】user1 中被用到的字段!
// 这里 name 被 move 到 user2,所以 user1.name 失效
// println!("{}", user1.name); // 编译错误!name 已转移
// 但 user1.active 仍可用(它是 Copy 类型,bool)
println!("{}", user1.active); // OK
}
struct User {
name: String,
age: u32,
active: bool,
}重要细节:..user1 会move 旧实例中被用到的字段(非 Copy 类型)。所以使用更新语法后,旧实例中被 move 的字段就失效了,但 Copy 字段仍可用。这是所有权规则在结构体上的体现。
3. 元组结构体与单元结构体
除了命名结构体,Rust 还有两种结构体形式:
fn main() {
// 元组结构体:字段没有名字,只有类型
// 适合"语义化包装"某个类型,但又不想写一堆字段名
struct Color(i32, i32, i32); // RGB 颜色
struct Point(i32, i32, i32); // 3D 点
struct Kilometers(f64); // 包装距离
let red = Color(255, 0, 0);
let origin = Point(0, 0, 0);
let dist = Kilometers(3.5);
// 访问元组结构体字段,用 .0 .1 .2
println!("R={} G={} B={}", red.0, red.1, red.2);
println!("距离 {} 千米", dist.0);
// 注意:Color 和 Point 虽然内部类型相同,
// 但是不同的类型,不能互相赋值!
// let c: Color = origin; // 编译错误,类型不匹配
// 这就是"newtype 模式"——避免误用基础类型
}
// 单元结构体:没有任何字段
struct AlwaysEqual; // 常用于"实现 trait 但不需要存数据"
fn main2() {
let subject = AlwaysEqual;
// 通常配合 trait 使用,后面 trait 篇会讲
}关键点:
- 元组结构体:字段无名字,用 .0 .1 .2 访问。适合"语义化包装",如 Color(255, 0, 0)。
- 不同元组结构体是不同类型:即使内部类型相同,Color 和 Point 也不能互转。这避免了"把距离当温度用"这类错误,叫newtype 模式。
- 单元结构体:没有任何字段。主要用于实现 trait(下一篇讲),当作"标记类型"。
4. impl 块:给结构体加方法
这是 Rust 的面向对象方式——把数据(struct)和行为(impl)分开。impl 块为结构体定义方法和关联函数:
struct Rectangle {
width: f64,
height: f64,
}
// impl 块:给结构体定义方法/关联函数
impl Rectangle {
// 关联函数(没有 self 参数):类似其他语言的"静态方法"
// 用 :: 调用,如 Rectangle::new(...)
fn new(width: f64, height: f64) -> Rectangle {
Rectangle { width, height }
}
// 正方形工厂方法
fn square(size: f64) -> Rectangle {
Rectangle { width: size, height: size }
}
// 方法:&self 表示"借用 self"(只读)
fn area(&self) -> f64 {
self.width * self.height
}
// 方法:&mut self 表示"可变借用 self"(能改字段)
fn scale(&mut self, factor: f64) {
self.width *= factor;
self.height *= factor;
}
// 方法:self(按值):消耗所有权,适合转换
fn into_square(self) -> Rectangle {
let size = (self.width + self.height) / 2.0;
Rectangle { width: size, height: size }
}
// 方法参数和返回值可以是别的类型
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
}
fn main() {
let mut rect = Rectangle::new(30.0, 50.0); // 关联函数用 :: 调用
println!("面积: {}", rect.area()); // 方法用 . 调用
rect.scale(2.0);
println!("放大后: {} x {}", rect.width, rect.height);
let rect2 = Rectangle::new(10.0, 20.0);
println!("rect 能装下 rect2? {}", rect.can_hold(&rect2));
let sq = Rectangle::square(25.0);
println!("正方形面积: {}", sq.area());
}几个核心概念:
- 方法:第一个参数是
self相关(&self、&mut self、self),用.调用。 - 关联函数:没有 self 参数,类似其他语言的"静态方法",用双冒号调用。常用于构造函数(惯例命名
new)。 - &self:只读借用,最常见。方法只读不改时用。
- &mut self:可变借用,方法需要修改字段时用。
- self:按值,消耗所有权。适合转换(如把 self 变成另一个对象)。
注意:Rust 没有"构造函数"语法糖。惯例是写一个名为 new 的关联函数,但它和别的关联函数没有本质区别——只是命名约定。
5. 多个 impl 块
一个结构体可以有多个 impl 块。这通常用于组织代码——把相关方法分组。功能上等价于一个大的 impl 块:
struct User {
name: String,
age: u32,
}
// 一个结构体可以有多个 impl 块(通常用于组织代码)
impl User {
fn new(name: &str, age: u32) -> User {
User { name: name.to_string(), age }
}
}
impl User {
fn greet(&self) -> String {
format!("你好,我是 {},今年 {} 岁", self.name, self.age)
}
}
impl User {
fn birthday(&mut self) {
self.age += 1;
println!("{} 过生日啦!", self.name);
}
}
fn main() {
let mut u = User::new("小明", 20);
println!("{}", u.greet());
u.birthday();
println!("{}", u.greet());
}这在大型项目里很有用。比如把"业务方法"放一个 impl,把"序列化方法"放另一个 impl,代码更清晰。
6. 打印结构体(Debug trait)
结构体默认不能用 println! 直接打印。需要派生 Debug trait:
#[derive(Debug)] // 让结构体可以用 {:?} 打印
struct Point { x: i32, y: i32 }
fn main() {
let p = Point { x: 1, y: 2 };
// 默认结构体不能直接 println! 打印
// 需要派生 Debug trait
println!("{:?}", p); // Point { x: 1, y: 2 }
println!("{:#?}", p); // 美化多行打印
// 更友好的输出:实现 Display trait(后面 trait 篇讲)
// 或写自己的 to_string 方法
}
// 常用派生 trait:
// Debug — 调试输出 {:?}
// Clone — 调用 clone() 深拷贝
// Copy — 按位复制(所有字段都要 Copy)
// PartialEq — 用 == 比较
// Hash — 可作为 HashMap 的 key
#[derive(Debug, Clone, PartialEq)]
struct Color { r: u8, g: u8, b: u8 }常用派生:
- Debug:调试输出
{:?:}。开发调试必备。 - Clone:支持
clone()深拷贝。 - Copy:按位复制(要求所有字段都 Copy)。
- PartialEq:支持
==比较。 - Hash:可作为 HashMap 的 key。
派生(derive)是 Rust 编译器的"代码生成"功能,自动为你实现这些 trait。后面 Trait 篇会深入。
7. 方法链式调用
如果方法返回 self(或修改后的 self),可以链式调用,类似 jQuery/Builder 模式:
- 方法返回
&mut Self或Self时,可以obj.method1().method2().method3()。 - 这是构建器(Builder)模式的基础,在标准库和第三方库中非常常见。
- 例如对
String调用new创建后,连续调用push_str添加内容(这里仅举例;真正链式的是返回 Self 的方法)。
8. 所有权与方法
方法接收 self 的方式,直接对应所有权语义:
- &self:借来用一下,不拿所有权。最常用。
- &mut self:借来改一下,不拿所有权。
- self:拿走所有权。方法结束后 self 被释放(或转换成别的)。
选择哪种?默认用 &self;需要改用 &mut self;需要消耗/转换才用 self。这遵循"最小权限"原则,让代码意图清晰。
小结
这一篇你掌握了 Rust 组织数据的全部工具:命名结构体(具名字段,最常用)、元组结构体(语义化包装)、单元结构体(标记类型)、impl 块(定义方法和关联函数)、派生 trait(Debug/Clone 等)。Rust 用 struct + impl 替代了传统"类",把数据和 behavior 分开,更符合"组合优于继承"的现代理念。
下一篇我们看枚举与模式匹配——Rust 的枚举远比 C/Java 强大,是处理"多种可能"的利器,配合 match 表达式威力无穷。
← 上一篇 Rust 字符串
下一篇 Rust 枚举与模式匹配 →