4、复合类型:元组、数组、结构体、枚举、模式匹配、Option与Result

Rust 的类型系统,说实话,是我用过的语言里最「舒服」的之一。它不像 C 那样原始,也不像 Python 那样过于动态。它给了你足够的表达能力,同时又牢牢守住内存安全的底线。

这一章我们聊聊复合类型。说白了,就是把多个值打包成一个整体。Rust 提供了几种不同的打包方式,每种都有自己的脾气。

4.1 元组:临时打包的好帮手

元组,我个人的理解是「临时组合」。它可以把不同类型的值塞到一起,用起来很轻量。

let person: (&str, u8, bool) = ("张三", 30, true);
println!("姓名: {}, 年龄: {}, 是否在职: {}", person.0, person.1, person.2);

访问元组里的元素,用点号加索引。从 0 开始。嗯,这里要注意:元组的长度是固定的,你不能往里面追加元素。

我在项目中遇到过一种场景:函数需要返回多个值,又不想专门定义一个结构体。这时候元组就派上用场了。

fn split_name(full_name: &str) -> (&str, &str) {
    let parts: Vec<&str> = full_name.split_whitespace().collect();
    (parts[0], parts[1])
}

你想想看,如果每个临时组合都要定义结构体,代码会变得多臃肿?

4.2 数组:固定长度的连续内存

数组在 Rust 里,长度是类型的一部分。这意味着 [i32; 3][i32; 4] 是两种不同的类型。

let numbers: [i32; 5] = [1, 2, 3, 4, 5];
let zeros = [0; 100]; // 100 个 0

数组存储在栈上,访问速度极快。但代价就是不够灵活。我曾经在做一个嵌入式项目时,需要固定大小的缓冲区,数组就是最佳选择。

注意:数组越界访问在 Rust 中会直接 panic。这和 C 语言那种「悄悄踩内存」的行为完全不同。Rust 在运行时做了边界检查,虽然有一点点性能开销,但换来了安全性。

4.3 结构体:自定义数据聚合

结构体是 Rust 里最常用的复合类型。它比元组更清晰,因为每个字段都有名字。

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

let user1 = User {
    username: String::from("alice"),
    email: String::from("alice@example.com"),
    sign_in_count: 1,
    active: true,
};

我个人习惯用结构体来建模业务实体。比如用户、订单、商品这些。字段名本身就是文档,读代码的人一眼就能明白每个字段的含义。

Rust 还支持元组结构体——就是字段没有名字,只有类型。

struct Color(i32, i32, i32);
let black = Color(0, 0, 0);

这种写法适合「这个值本质上就是一个包装」的场景。比如 RGB 颜色、坐标点等。

4.4 枚举:表达「要么是 A,要么是 B」

枚举是 Rust 类型系统的精髓之一。它比 C 语言的枚举强大得多,因为每个变体可以携带数据。

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

你看,Quit 没有数据,Move 带了两个命名字段,Write 带了一个 String,ChangeColor 带了三个 i32。这种灵活性,在 Java 里你得用继承体系才能实现。

我记得有一次重构一个支付系统,不同支付方式(微信、支付宝、银行卡)的处理逻辑完全不同。用枚举来表达支付方式,配合模式匹配,代码变得异常清晰。

4.5 模式匹配:Rust 的 switch 升级版

模式匹配是 Rust 里我最喜欢的特性之一。它不仅仅是 switch,它还能解构数据。

fn process_message(msg: Message) {
    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),
    }
}

模式匹配是穷尽的——编译器会检查你是否覆盖了所有分支。这能避免很多运行时错误。我曾经因为漏掉一个分支,导致线上服务在某种边界条件下崩溃。从那以后,我对 match 的穷尽性检查格外感激。

小技巧:如果你只关心某一个分支,可以用 if let 语法糖。比如 if let Message::Write(text) = msg { ... },比写完整的 match 更简洁。

4.6 Option:告别空指针

Tony Hoare 把空指针称为「十亿美元的错误」。Rust 用 Option<T> 彻底解决了这个问题。

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

就是这么简单。一个值要么存在(Some),要么不存在(None)。你没法直接使用一个可能为空的值,必须先用模式匹配把它「拆开」。

fn find_user(id: u32) -> Option<String> {
    if id == 1 {
        Some(String::from("Alice"))
    } else {
        None
    }
}

let result = find_user(2);
match result {
    Some(name) => println!("找到用户: {}", name),
    None => println!("用户不存在"),
}

你想想看,在 Java 里你写 String name = findUser(2);,然后 name.length() 直接空指针崩溃。在 Rust 里,编译器逼着你处理 None 的情况。这不是限制,这是保护。

4.7 Result:错误处理的正确姿势

如果说 Option 处理的是「有或无」,那 Result 处理的就是「成功或失败」。

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

Result 在 Rust 里无处不在。文件读取、网络请求、JSON 解析……几乎所有可能失败的操作都返回 Result。

use std::fs::File;
use std::io::Read;

fn read_file(path: &str) -> Result<String, std::io::Error> {
    let mut file = File::open(path)?;
    let mut contents = String::new();
    file.read_to_string(&mut contents)?;
    Ok(contents)
}

注意那个 ? 操作符。它是 Rust 里的语法糖:如果 Result 是 Err,就提前返回;如果是 Ok,就取出里面的值继续执行。这比写一堆 match 要简洁得多。

核心思想:Option 和 Result 把「可能为空」和「可能失败」这两个概念显式地编码到了类型系统里。你没法忽略它们,编译器会盯着你。这种设计让 Rust 代码在运行时几乎不会出现「意料之外的崩溃」。

4.8 知识体系总览

下面这张图展示了本章的核心逻辑。你可以看到,复合类型从「固定大小」到「灵活表达」,层层递进。

Rust 复合类型知识体系 复合类型 固定大小 自定义聚合 安全表达 元组 数组 结构体 枚举 模式匹配 Option Result 从左到右:从固定大小 → 自定义聚合 → 安全表达,层层递进

从这张图可以看得很清楚:元组和数组是「固定大小」的代表,结构体和枚举是「自定义聚合」的主力,而 Option 和 Result 则是「安全表达」的基石。模式匹配贯穿其中,是操作这些类型的核心工具。


好了,这一章的内容就到这里。复合类型是 Rust 类型系统的骨架,理解它们之间的区别和适用场景,能让你写出更安全、更清晰的代码。下一章我们会聊聊所有权和借用——这才是 Rust 真正与众不同的地方。

专注资料整理