FORMA

第四章:复合数据类型

结构体、枚举与集合类型是 Rust 构建复杂数据模型的核心工具。这一章你将学会如何定义自定义类型、利用模式匹配优雅地解构数据,以及选用合适的标准集合来高效组织数据。

4.1 结构体(struct)

结构体是自定义数据类型的基本方式,可以把多个相关的值打包在一起,并为每个字段命名,让代码意涵更清晰。

定义与实例化

定义一个结构体:

rust
struct User {
    active: bool,
    username: String,
    sign_in_count: u64,
}

创建实例时,字段顺序不必与定义一致,但所有字段都必须赋值:

rust
let user1 = User {
    active: true,
    username: String::from("alice"),
    sign_in_count: 1,
};

通过点号访问字段:

rust
println!("{}", user1.username);

只有把整个实例声明为 mut 才能修改字段,Rust 不允许只将部分字段标记为可变。

rust
let mut user2 = User {
    active: true,
    username: String::from("bob"),
    sign_in_count: 2,
};
user2.sign_in_count += 1;

字段初始化简写与结构体更新语法

如果变量名和字段名相同,可以省略冒号和值:

rust
fn build_user(username: String) -> User {
    User {
        active: true,
        username,              // 简写
        sign_in_count: 1,
    }
}

想基于已有实例创建新实例,只修改部分字段时,使用 .. 更新语法:

rust
let user3 = User {
    username: String::from("charlie"),
    ..user2            // 其余字段使用 user2 的值
};

注意:..user2 会移动 user2 中未实现 Copy 的字段(比如 String),此后 user2 不能再整体使用,但未移动的字段依然有效。

元组结构体与单元结构体

元组结构体 没有字段名,只有类型:

rust
struct Color(i32, i32, i32);
let black = Color(0, 0, 0);
println!("{}", black.0); // 通过索引访问

单元结构体 不保存任何数据,仅作为标记类型,常用于实现 trait:

rust
struct AlwaysEqual;
let subject = AlwaysEqual;

方法与关联函数

impl 块为结构体定义方法。方法的第一个参数是 self 的某种形式:

  • &self:不可变借用,读取数据。
  • &mut self:可变借用,修改数据。
  • self:获取所有权,消耗结构体。
rust
impl User {
    fn deactivate(&mut self) {
        self.active = false;
    }
}

关联函数 不以 self 为参数,通常用作构造函数,调用时用 ::

rust
impl User {
    fn new(username: String) -> Self {
        User {
            active: true,
            username,
            sign_in_count: 1,
        }
    }
}
let u = User::new(String::from("dave"));

调试输出

通过为结构体添加 #[derive(Debug)] 属性,可以自动实现 Debug trait,方便打印调试信息:

rust
#[derive(Debug)]
struct User { /* ... */ }
println!("{:?}", user1);

4.2 枚举(enum)与模式匹配

枚举让你定义一个类型,其值可以是几个固定变体之一,且每个变体可以携带不同类型和大小的数据,比很多语言中的联合体强大得多。

枚举定义

一个典型的消息枚举:

rust
enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(i32, i32, i32),
}

每个变体可以:

  • 无数据(Quit
  • 带匿名字段(Write(String)
  • 带命名字段(Move { x, y }

枚举的强大之处在于:一个值可以是这些变体中的任何一个,但处理它时必须通过模式匹配穷尽所有可能。

Option<T>:用类型系统取代空指针

Option 是标准库预定义的枚举,表达了“要么有值,要么什么都没有”:

rust
enum Option<T> {
    None,
    Some(T),
}

你不需要手动 useOption::SomeOption::None 直接可用。对一个 Option,必须显式处理 None 情况,从编译期避免空指针问题:

rust
let x: Option<i32> = Some(5);
let y: Option<i32> = None;

Result<T, E>:错误处理的核心

Result 表示可能失败的操作:

rust
enum Result<T, E> {
    Ok(T),
    Err(E),
}

处理 Result 时也要面对成功和失败两条路径:

rust
let f = std::fs::File::open("file.txt");
match f {
    Ok(file) => { /* 使用文件 */ }
    Err(error) => { /* 处理错误 */ }
}

后续章节会深入 Result 的组合子和传播方式。

match 表达式

match 与枚举天然契合。它会强制检查所有变体(穷尽性检查),每个分支叫一个“臂”(arm)。模式可以包含解构、或运算符、范围、守卫等。

rust
match msg {
    Message::Quit => println!("退出"),
    Message::Move { x, y } => println!("移动到 ({}, {})", x, y),
    Message::Write(text) => println!("写入: {}", text),
    Message::ChangeColor(r, g, b) => println!("改变颜色: ({}, {}, {})", r, g, b),
}

多模式与范围

| 在一个臂中匹配多个模式:

rust
match x {
    1 | 2 => println!("一或二"),
    _ => println!("其他"),
}

匹配范围(要求值类型支持):

rust
match x {
    1..=5 => println!("1到5"),
    _ => println!("其他"),
}

匹配守卫

在模式后加上 if 条件来进一步过滤:

rust
match some_number {
    n if n % 2 == 0 => println!("偶数"),
    _ => println!("奇数"),
}

if letwhile let

如果只关心一种模式,可以使用更简洁的 if let

rust
if let Message::Write(text) = msg {
    println!("写入 {text}");
} else {
    println!("不是写入消息");
}

while let 则持续匹配,常用于迭代器或通道:

rust
while let Some(top) = stack.pop() {
    println!("{top}");
}

解构

模式匹配的强大离不开解构。你可以随手拆开复合数据。

元组解构:

rust
let pair = (1, "hello");
let (x, y) = pair;
println!("{} {}", x, y);

结构体解构:

rust
struct Point { x: i32, y: i32 }
let p = Point { x: 10, y: 20 };
let Point { x, y } = p;

也可以部分解构或重命名:

rust
let Point { x: a, y: b } = p;  // a == 10, b == 20

4.3 集合类型

Rust 标准库提供了多种通用集合类型,它们分配在堆上,可以动态增长。

Vec<T>:动态数组

Vec 是最常用的集合,可以存储一系列相同类型的值,按需自动扩容。

rust
let mut v: Vec<i32> = Vec::new();
v.push(1);
v.push(2);

使用 vec! 宏创建并初始化:

rust
let v = vec![1, 2, 3, 4, 5];

访问元素有两种方式:

  • 索引 &v[2]:越界时会 panic。
  • v.get(2) 返回 Option<&T>,安全处理。
rust
match v.get(10) {
    Some(value) => println!("{value}"),
    None => println!("没有这个元素"),
}

遍历:

rust
for i in &v {
    println!("{i}");
}
for i in &mut v {
    *i += 1;   // 通过解引用指针修改值
}

Vec 可以通过切片视图访问其全部或部分内容:

rust
let slice: &[i32] = &v[1..3];

String&str

Rust 的字符串相当严谨。String 是一个可增长的、拥有所有权的 UTF-8 编码字符串,分配在堆上。

rust
let mut s = String::from("hello");
s.push_str(", world");
s.push('!');

+ 拼接时,左侧必须是 String,右侧是 &str,且会获取左侧的所有权:

rust
let s1 = String::from("hello");
let s2 = String::from(" world");
let s3 = s1 + &s2;   // s1 所有权被移走,此后不可再用 s1
// println!("{s1}"); // 编译错误

format! 按引用格式化,不拿走所有权,更灵活:

rust
let hello = String::from("hello");
let world = String::from("world");
let s = format!("{}-{}", hello, world); // hello、world 仍可使用

&str 是字符串切片,本质是对内存某段有效 UTF-8 数据的引用。字符串字面量就是 &str

rust
let literal: &str = "I'm static";

许多 API 使用 &str 作为参数,因为 &String 会自动强制转换为 &str,让接口更加通用。

HashMap<K, V>

哈希映射存储键值对,键类型必须实现 EqHash trait。

rust
use std::collections::HashMap;

let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);

通过 get 按键取值,返回 Option<&V>

rust
let team = String::from("Blue");
let score = scores.get(&team).copied().unwrap_or(0);

更新值的方法:

  • insert 会覆盖旧值。
  • entry API 根据键是否存在,决定插入还是修改:
rust
scores.entry(String::from("Blue")).or_insert(50); // 存在则不动
scores.entry(String::from("Red")).or_insert(30);  // 不存在则插入

entry 返回的可变引用还可以对旧值进行更新:

rust
let count = scores.entry(String::from("Blue")).or_insert(0);
*count += 10;

遍历:

rust
for (key, value) in &scores {
    println!("{key}: {value}");
}

其他集合类型

Rust 提供的最常用集合还包括:

  • HashSet<T>:基于 HashMap 的集合,保证元素唯一,提供高效的插入、查询和删除。
  • VecDeque<T>:双端队列,可在头尾高效地插入和移除,内部是环形缓冲。
  • LinkedList<T>:双向链表,适合在任意位置频繁插入、分割列表的场景。但在连续内存遍历方面性能不如 VecDeque
  • BinaryHeap<T>:基于二叉堆的最大优先队列,peek 返回最大元素。

选择集合时要考虑访问模式、性能特性和内存布局。

迭代器(Iterator)初步

迭代器是处理集合的统一抽象,Rust 的迭代器零开销且功能极其强大。

三种产生迭代器的常用方法:

  • .iter():返回不可变引用的迭代器,不消耗集合。
  • .iter_mut():返回可变引用的迭代器。
  • .into_iter():获取所有权,消耗集合并产生元素。
rust
let v = vec![1, 2, 3];
for val in v.iter() {
    println!("{val}");
}
// v 仍然可用

迭代器可以链式调用各种适配器(mapfiltercollect 等),后续章节将详细介绍。这里只需知道:for 循环本质上是 into_iter() 的语法糖,而标准库中的集合都实现了 IntoIterator,让它们能自然地在循环中工作。

结构体让你把数据组织成有意义的整体,枚举使类型能承载多种可能状态,而集合则负责高效存储和遍历成批数据。下一章请阅读 泛型与 Trait

参考文献

资料说明
Structs官方书第 5 章
Enums枚举与 match
CollectionsVecStringHashMap

相关文章