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
数组存储在栈上,访问速度极快。但代价就是不够灵活。我曾经在做一个嵌入式项目时,需要固定大小的缓冲区,数组就是最佳选择。
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 要简洁得多。
4.8 知识体系总览
下面这张图展示了本章的核心逻辑。你可以看到,复合类型从「固定大小」到「灵活表达」,层层递进。
从这张图可以看得很清楚:元组和数组是「固定大小」的代表,结构体和枚举是「自定义聚合」的主力,而 Option 和 Result 则是「安全表达」的基石。模式匹配贯穿其中,是操作这些类型的核心工具。
好了,这一章的内容就到这里。复合类型是 Rust 类型系统的骨架,理解它们之间的区别和适用场景,能让你写出更安全、更清晰的代码。下一章我们会聊聊所有权和借用——这才是 Rust 真正与众不同的地方。