09. Structs — 结构体
结构体(struct)是 Rust 中将多个相关值组合成一个复合类型的基本方式。与元组不同,结构体为每个值赋予一个有意义的名字,使代码更具可读性。如果说元组是“把几个东西捆在一起”,那结构体就是“给每个东西都贴上标签”。
Rust 的结构体有三种形式:命名字段结构体、元组结构体和单元结构体,覆盖了从简单数据聚合到复杂抽象的所有场景。
三种结构体
命名字段结构体
这是最常用的结构体形式——每个字段都有名字和类型:
/// 灰度图像:像素从 0(黑)到 255(白)
struct GrayscaleMap {
pixels: Vec<u8>,
width: usize,
height: usize,
}创建实例用大括号语法,字段顺序无所谓:
let map = GrayscaleMap {
pixels: vec![0; 480 * 640],
width: 640,
height: 480,
};
// 访问字段
assert_eq!(map.width, 640);
assert_eq!(map.height, 480);与 C/C++ 不同,Rust 的结构体字段默认是私有的——即使在同一个 crate 中。只有显式标注 pub 的字段才能在结构体所在模块之外访问:
pub struct GrayscaleMap {
pub pixels: Vec<u8>, // 外部可以直接访问
width: usize, // 仅模块内部可访问
height: usize, // 仅模块内部可访问
}这种设计与 Java/C++ 需要大量 getter/setter boilerplate 的做法截然不同——你只需要把允许外部访问的字段标为 pub,把需要不变量保护的字段保持私有。
元组结构体
元组结构体的字段没有名字,只有类型——本质上是一个命名的元组:
struct Bounds(usize, usize);
struct Ascii(Vec<u8>);创建和访问:
// 创建
let image_bounds = Bounds(1024, 768);
// 通过位置索引访问
assert_eq!(image_bounds.0, 1024);
assert_eq!(image_bounds.1, 768);
// 可以解构
let Bounds(w, h) = image_bounds;
println!("{w}x{h}"); // 1024x768元组结构体适用于两种情况:
- 你想要给某个类型一个独立的名字,但字段名没有额外价值(如
Bounds(usize, usize)) - 你想要实现 Newtype 模式——用一个单字段结构体包装现有类型,赋予新的语义
// Newtype 模式:类型级别的区分
struct Inches(f64);
struct Centimeters(f64);
let length = Inches(10.0);
let width = Centimeters(30.0);
// length + width // 编译错误!类型不同不能相加Newtype 模式在 Rust 中非常常见——它用零运行时开销实现了类型安全的包装。String 实际上就是 Vec<u8> 的 newtype(只暴露保证 UTF-8 有效的操作)。
单元结构体
单元结构体没有任何字段,只有名字:
struct Onesuch; // 没有任何字段
struct SpaceBeforeNext; // 常用于标记类型它们类似于 C 语言中不完整类型的用途——作为一个标记,用于需要类型但不需要数据的场景。配合 trait 使用时尤为常见:
struct GlobalState;
impl GlobalState {
fn configure() { /* ... */ }
}impl 块:方法与关联函数
结构体本身定义数据,impl 块定义行为:
pub struct Queue {
older: Vec<char>,
younger: Vec<char>,
}
impl Queue {
/// 关联函数(静态方法):构造器
pub fn new() -> Queue {
Queue {
older: Vec::new(),
younger: Vec::new(),
}
}
/// 方法:需要 &self 或 &mut self
pub fn push(&mut self, c: char) {
self.younger.push(c);
}
/// 可变引用方法
pub fn pop(&mut self) -> Option<char> {
if self.older.is_empty() {
if self.younger.is_empty() {
return None;
}
use std::mem::swap;
swap(&mut self.older, &mut self.younger);
self.older.reverse();
}
self.older.pop()
}
/// 共享引用方法
pub fn len(&self) -> usize {
self.older.len() + self.younger.len()
}
/// 消耗 self 的方法——调用后 self 不再可用
pub fn split(self) -> (Vec<char>, Vec<char>) {
(self.older, self.younger)
}
}self 接收者的三种形式
| 接收者 | 含义 | 类比 |
|---|---|---|
&self | 共享引用,只读访问 | C++ 的 const 方法 |
&mut self | 可变引用,可修改 | C++ 的普通方法 |
self | 获取所有权,消耗值 | 移动语义,调用后值不再可用 |
self 可以带类型前缀用作智能指针接收者,但这种用法比较少见:
impl Queue {
// 通过 Rc<Self> 调用方法
pub fn push(self: Rc<Self>, c: char) { /* ... */ }
// 通过 Box<Self> 调用方法
pub fn pop(self: Box<Self>) -> Option<char> { /* ... */ }
}关联函数 vs 方法
关联函数(associated function)不以 self 为参数——调用时用 :: 而不是 .:
// 关联函数:Queue::new()
let mut q = Queue::new();
// 方法:用 . 调用
q.push('a');
q.push('b');
assert_eq!(q.pop(), Some('a'));常见的关联函数包括构造器(new)、工厂函数和常量访问器。
关联常量
impl 块还可以定义关联常量:
impl Queue {
const INITIAL_CAPACITY: usize = 64;
const MAX_CAPACITY: usize = 1_000_000;
}
// 通过类型名访问
let capacity = Queue::INITIAL_CAPACITY;泛型结构体
结构体可以是泛型的——类型参数在结构体名后声明:
pub struct Queue<T> {
older: Vec<T>,
younger: Vec<T>,
}相应的 impl 块也需要声明类型参数:
impl<T> Queue<T> {
pub fn new() -> Queue<T> {
Queue {
older: Vec::new(),
younger: Vec::new(),
}
}
pub fn push(&mut self, item: T) {
self.younger.push(item);
}
}你可以为特定类型参数编写专门的 impl 块:
// 为 Queue<f64> 添加求和方法(仅对 f64 有意义)
impl Queue<f64> {
fn sum(&self) -> f64 {
self.older.iter().chain(self.younger.iter()).sum()
}
}带有生命周期参数的结构体
如果结构体包含引用,则需要声明生命周期参数:
struct Extrema<'elt> {
greatest: &'elt i32,
least: &'elt i32,
}
fn find_extrema<'s>(slice: &'s [i32]) -> Extrema<'s> {
let mut greatest = &slice[0];
let mut least = &slice[0];
for item in slice {
if *item > *greatest { greatest = item; }
if *item < *least { least = item; }
}
Extrema { greatest, least }
}生命周期参数确保 Extrema 中的引用不会比被引用的数据活得更久。生命周期声明必须出现在类型参数之前:
struct RefWithType<'a, T> {
reference: &'a T,
description: String,
}结构体更新语法
当你想从已有结构体创建新实例时,用 .. 语法:
let q1 = Queue::new();
// ... 给 q1 添加了一些元素 ...
// 新队列复用 q1 的 older 和 younger
let q2 = Queue {
older: Vec::new(),
..q1 // 其余字段从 q1 复制/移动
};注意:.. 会移动非 Copy 类型的字段!上例中 q1.younger 被移动到 q2,此后 q1.younger 无法再被访问。如果结构体实现了 Copy,则原实例仍然可用。
.. 也常用于 Default 模式:
#[derive(Default)]
struct Config {
host: String,
port: u16,
timeout: u64,
}
// 大部分用默认值,只覆盖需要的字段
let config = Config {
host: "localhost".to_string(),
..Default::default()
};结构体的内存布局
Rust 结构体的字段在内存中是内联存放的——不像 JavaScript 或 Python 那样字段是堆上对象的引用。这与 C 的结构体布局相同:
// GrayscaleMap 的内存布局
// [pixels: 指针+容量+长度(24字节)][width: 8字节][height: 8字节]
// 总计:40 字节(在 64 位系统上)Rust 不保证结构体字段在内存中恰好按声明的顺序排列——编译器可以为了对齐而重新排列字段。如果你想与 C 代码交互并需要确定的布局,使用 #[repr(C)]:
#[repr(C)]
struct ControlBlock {
status: u32, // 偏移 0
data: *mut u8, // 偏移 4 或 8(取决于对齐)
count: u64, // 偏移 8 或 16
}#[repr(C)] 保证 C 兼容的布局(字段按声明顺序、C 的对齐规则),但会禁用 Rust 默认的布局优化。
派生常见 Trait
大多数结构体都需要一些标准 trait 的实现。Rust 提供了 #[derive] 属性来自动生成这些代码:
#[derive(Copy, Clone, Debug, PartialEq)]
struct Point {
x: f64,
y: f64,
}常用的可派生 trait:
| Trait | 作用 | 何时需要 |
|---|---|---|
Debug | 调试输出 {:?} | 几乎所有结构体 |
Clone | 显式克隆 clone() | 需要深拷贝 |
Copy | 隐式按位复制 | 简单值类型(无堆数据) |
PartialEq | == 和 != 比较 | 需要相等比较 |
Eq | 标记全等关系 | 配合 PartialEq 表示数学上的等价关系 |
PartialOrd | <、>、<=、>= 比较 | 需要排序 |
Hash | 哈希计算 | 用于 HashMap 的键 |
Default | 默认值 Default::default() | 提供合理的“零值” |
注意:Copy 和 Drop 不能同时实现——编译器会给 Copy 类型添加隐式的按位复制,而 Drop 类型的复制语义可能破坏资源管理。
内部可变性
通常,Rust 的借用规则强制“要么一个可变引用,要么多个共享引用”。但对于某些需要“通过共享引用修改内部状态”的场景,标准库提供了两个包装类型:
Cell<T>
适用于 Copy 类型——通过 get()/set() 进行值的整体替换,不暴露内部引用:
use std::cell::Cell;
struct SpiderRobot {
hardware_error_count: Cell<u32>,
}
impl SpiderRobot {
fn add_hardware_error(&self) { // &self 而非 &mut self!
let n = self.hardware_error_count.get();
self.hardware_error_count.set(n + 1);
}
}Cell<T> 永远不会给你 &T 或 &mut T 引用——它只能通过值拷贝来读取和写入。因此它只适用于 Copy 类型,但完全没有运行时开销。
RefCell<T>
适用于任何类型——在运行时执行借用检查,而非编译时:
use std::cell::RefCell;
struct SpiderRobot {
log: RefCell<Vec<String>>,
}
impl SpiderRobot {
fn log_message(&self, msg: &str) {
// borrow_mut() 在运行时检查:没有其他借用才能返回 &mut
self.log.borrow_mut().push(msg.to_string());
}
fn last_log(&self) -> Option<String> {
// borrow() 在运行时检查:没有可变借用才能返回 &
self.log.borrow().last().cloned()
}
}RefCell<T> 的借用规则与编译时一致(不能同时有可变和共享引用),但违反时不会出现编译错误——而是 panic。RefCell 非常适合“逻辑上不可变、需要延迟初始化或内部缓存”的场景。
实战:构建一个图像处理结构体
以下是一个综合示例,展示了结构体、方法、泛型和 trait 实现的组合:
#[derive(Debug, Clone)]
pub struct Image<P> {
pixels: Vec<P>,
width: usize,
height: usize,
}
impl<P> Image<P> {
/// 创建纯色图像
pub fn new(width: usize, height: usize, color: P) -> Self
where
P: Copy,
{
Image {
pixels: vec![color; width * height],
width,
height,
}
}
/// 图像宽度
pub fn width(&self) -> usize {
self.width
}
/// 图像高度
pub fn height(&self) -> usize {
self.height
}
/// 获取像素引用
pub fn pixel(&self, x: usize, y: usize) -> Option<&P> {
if x < self.width && y < self.height {
Some(&self.pixels[y * self.width + x])
} else {
None
}
}
/// 获取可变像素引用
pub fn pixel_mut(&mut self, x: usize, y: usize) -> Option<&mut P> {
if x < self.width && y < self.height {
Some(&mut self.pixels[y * self.width + x])
} else {
None
}
}
}
impl<P> Default for Image<P>
where
P: Default + Copy,
{
fn default() -> Self {
Image::new(1, 1, P::default())
}
}与 C++ 的对比
| 概念 | C++ | Rust |
|---|---|---|
| 数据聚合 | struct | struct |
| 方法定义 | 类内声明 + 类外实现 | impl 块 |
| 静态方法 | static 成员函数 | 关联函数(无 self) |
| 构造函数 | 构造函数 ClassName(...) | 关联函数 new()(约定) |
| 析构函数 | ~ClassName() | Drop trait |
| const 方法 | int get() const; | fn get(&self) |
| 继承 | class Derived : public Base | 无(用 trait + 组合代替) |
| 访问控制 | public:/private:/protected: | pub 标记 + 默认私有 |
| 泛型 | template<typename T> | <T> + trait bound |
| 位域 | unsigned int flags : 4; | 用 bitflag crate 或手动位操作 |
| 内存布局控制 | 编译器决定(或 #pragma pack) | #[repr(C)] |
Rust 没有 C++ 式的继承——取而代之的是 trait 系统(下下章)和组合模式。C++ 中常见的“基类 + 虚函数”在 Rust 中用 trait 对象(dyn Trait)表达。
小结
- Rust 有三种结构体:命名字段结构体(最常见)、元组结构体(命名元组)和单元结构体(标记类型)。Newtype 模式用元组结构体实现零开销类型安全包装。
impl块定义行为:方法通过&self/&mut self/self接收者区分只读、可变和移动语义。关联函数(如new())用::调用。- 结构体字段默认私有:用
pub选择性公开,消除 getter/setter boilerplate。 - 泛型结构体通过
<T>参数化类型,配合生命周期参数<'a>安全地持有引用。 ..更新语法从已有实例复制剩余字段;注意非Copy字段会被移动。#[derive]自动生成常见 trait 实现,减少 boilerplate。- 内部可变性通过
Cell<T>(编译时零开销,仅适用于Copy类型)和RefCell<T>(运行时检查)实现“共享引用修改内部状态”。