Skip to content
Published at:

09. Structs — 结构体

结构体(struct)是 Rust 中将多个相关值组合成一个复合类型的基本方式。与元组不同,结构体为每个值赋予一个有意义的名字,使代码更具可读性。如果说元组是“把几个东西捆在一起”,那结构体就是“给每个东西都贴上标签”。

Rust 的结构体有三种形式:命名字段结构体元组结构体单元结构体,覆盖了从简单数据聚合到复杂抽象的所有场景。

三种结构体

命名字段结构体

这是最常用的结构体形式——每个字段都有名字和类型:

rust
/// 灰度图像:像素从 0(黑)到 255(白)
struct GrayscaleMap {
    pixels: Vec<u8>,
    width: usize,
    height: usize,
}

创建实例用大括号语法,字段顺序无所谓:

rust
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 的字段才能在结构体所在模块之外访问:

rust
pub struct GrayscaleMap {
    pub pixels: Vec<u8>,  // 外部可以直接访问
    width: usize,          // 仅模块内部可访问
    height: usize,         // 仅模块内部可访问
}

这种设计与 Java/C++ 需要大量 getter/setter boilerplate 的做法截然不同——你只需要把允许外部访问的字段标为 pub,把需要不变量保护的字段保持私有。

元组结构体

元组结构体的字段没有名字,只有类型——本质上是一个命名的元组:

rust
struct Bounds(usize, usize);
struct Ascii(Vec<u8>);

创建和访问:

rust
// 创建
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 模式——用一个单字段结构体包装现有类型,赋予新的语义
rust
// 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 有效的操作)。

单元结构体

单元结构体没有任何字段,只有名字:

rust
struct Onesuch;            // 没有任何字段
struct SpaceBeforeNext;    // 常用于标记类型

它们类似于 C 语言中不完整类型的用途——作为一个标记,用于需要类型但不需要数据的场景。配合 trait 使用时尤为常见:

rust
struct GlobalState;

impl GlobalState {
    fn configure() { /* ... */ }
}

impl 块:方法与关联函数

结构体本身定义数据,impl 块定义行为:

rust
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 可以带类型前缀用作智能指针接收者,但这种用法比较少见:

rust
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 为参数——调用时用 :: 而不是 .

rust
// 关联函数:Queue::new()
let mut q = Queue::new();

// 方法:用 . 调用
q.push('a');
q.push('b');
assert_eq!(q.pop(), Some('a'));

常见的关联函数包括构造器(new)、工厂函数和常量访问器。

关联常量

impl 块还可以定义关联常量:

rust
impl Queue {
    const INITIAL_CAPACITY: usize = 64;
    const MAX_CAPACITY: usize = 1_000_000;
}

// 通过类型名访问
let capacity = Queue::INITIAL_CAPACITY;

泛型结构体

结构体可以是泛型的——类型参数在结构体名后声明:

rust
pub struct Queue<T> {
    older: Vec<T>,
    younger: Vec<T>,
}

相应的 impl 块也需要声明类型参数:

rust
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 块:

rust
// 为 Queue<f64> 添加求和方法(仅对 f64 有意义)
impl Queue<f64> {
    fn sum(&self) -> f64 {
        self.older.iter().chain(self.younger.iter()).sum()
    }
}

带有生命周期参数的结构体

如果结构体包含引用,则需要声明生命周期参数:

rust
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 中的引用不会比被引用的数据活得更久。生命周期声明必须出现在类型参数之前:

rust
struct RefWithType<'a, T> {
    reference: &'a T,
    description: String,
}

结构体更新语法

当你想从已有结构体创建新实例时,用 .. 语法:

rust
let q1 = Queue::new();
// ... 给 q1 添加了一些元素 ...

// 新队列复用 q1 的 older 和 younger
let q2 = Queue {
    older: Vec::new(),
    ..q1    // 其余字段从 q1 复制/移动
};

注意:.. 会移动非 Copy 类型的字段!上例中 q1.younger 被移动到 q2,此后 q1.younger 无法再被访问。如果结构体实现了 Copy,则原实例仍然可用。

.. 也常用于 Default 模式:

rust
#[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)]

rust
#[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] 属性来自动生成这些代码:

rust
#[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()提供合理的“零值”

注意:CopyDrop 不能同时实现——编译器会给 Copy 类型添加隐式的按位复制,而 Drop 类型的复制语义可能破坏资源管理。

内部可变性

通常,Rust 的借用规则强制“要么一个可变引用,要么多个共享引用”。但对于某些需要“通过共享引用修改内部状态”的场景,标准库提供了两个包装类型:

Cell<T>

适用于 Copy 类型——通过 get()/set() 进行值的整体替换,不暴露内部引用:

rust
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>

适用于任何类型——在运行时执行借用检查,而非编译时:

rust
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> 的借用规则与编译时一致(不能同时有可变和共享引用),但违反时不会出现编译错误——而是 panicRefCell 非常适合“逻辑上不可变、需要延迟初始化或内部缓存”的场景。

实战:构建一个图像处理结构体

以下是一个综合示例,展示了结构体、方法、泛型和 trait 实现的组合:

rust
#[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
数据聚合structstruct
方法定义类内声明 + 类外实现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>(运行时检查)实现“共享引用修改内部状态”。