结构体与枚举
问题
Rust 的结构体和枚举有什么特点?Option 和 Result 的设计原理是什么?
答案
结构体(struct)和枚举(enum)是 Rust 自定义类型的两大基石。Rust 没有传统的类(class),而是用 struct + enum + trait 组合实现数据建模。
结构体(Struct)
三种形式
// 1. 命名字段结构体(最常用)
struct User {
name: String,
email: String,
age: u32,
active: bool,
}
// 2. 元组结构体(Tuple Struct)
struct Color(u8, u8, u8);
struct Meters(f64); // Newtype 模式
// 3. 单元结构体(Unit Struct)
struct Marker; // 大小为 0,用作标记类型
创建与使用
fn main() {
// 创建实例
let user = User {
name: String::from("Alice"),
email: String::from("alice@example.com"),
age: 30,
active: true,
};
// 字段初始化简写(变量名和字段名相同时)
let name = String::from("Bob");
let email = String::from("bob@example.com");
let user2 = User { name, email, age: 25, active: true };
// 结构体更新语法(从已有实例创建)
let user3 = User {
name: String::from("Charlie"),
..user2 // 其余字段从 user2 来(注意 move 语义!)
};
// user2.email 已被 move(String 不是 Copy),但 user2.age 仍可访问
}
方法与关联函数
struct Rectangle {
width: f64,
height: f64,
}
impl Rectangle {
// 关联函数(类似静态方法),无 self,用 :: 调用
fn new(width: f64, height: f64) -> Self {
Rectangle { width, height }
}
fn square(size: f64) -> Self {
Rectangle { width: size, height: size }
}
// 方法:&self = 不可变借用
fn area(&self) -> f64 {
self.width * self.height
}
// 方法:&mut self = 可变借用
fn scale(&mut self, factor: f64) {
self.width *= factor;
self.height *= factor;
}
// 方法:self = 获取所有权(消耗自身)
fn into_square(self) -> Rectangle {
let side = (self.width + self.height) / 2.0;
Rectangle { width: side, height: side }
}
}
fn main() {
let mut rect = Rectangle::new(10.0, 5.0); // 关联函数
println!("面积: {}", rect.area()); // 50.0
rect.scale(2.0); // 可变方法
println!("缩放后面积: {}", rect.area()); // 200.0
}
常用 Derive 宏
#[derive(Debug, Clone, PartialEq)]
struct Point {
x: f64,
y: f64,
}
// Debug: 启用 {:?} 格式化打印
// Clone: 可显式 .clone()
// PartialEq: 可用 == 比较
// 其他常用:Copy, Hash, Default, Serialize, Deserialize
枚举(Enum)
Rust 的枚举远比 C/Java 的枚举强大——每个变体(variant)可以携带不同类型的数据,这就是**代数数据类型(ADT)**中的"和类型(Sum Type)"。
基本定义
// 简单枚举
enum Direction {
North,
South,
East,
West,
}
// 携带数据的枚举
enum Message {
Quit, // 无数据
Move { x: i32, y: i32 }, // 命名字段(类似结构体)
Write(String), // 单一数据
ChangeColor(u8, u8, u8), // 多个数据(类似元组)
}
枚举方法
impl Message {
fn call(&self) {
match self {
Message::Quit => println!("退出"),
Message::Move { x, y } => println!("移动到 ({}, {})", x, y),
Message::Write(text) => println!("写入: {}", text),
Message::ChangeColor(r, g, b) => println!("颜色: ({}, {}, {})", r, g, b),
}
}
}
Option — Rust 的 null 替代品
Rust 没有 null,用 Option<T> 表示"值可能不存在":
enum Option<T> {
Some(T), // 有值
None, // 无值
}
强制开发者处理"值不存在"的情况——不处理就无法编译:
fn find_user(id: u32) -> Option<String> {
if id == 1 {
Some(String::from("Alice"))
} else {
None
}
}
fn main() {
let user = find_user(1);
// 方法 1:match
match user {
Some(name) => println!("找到: {}", name),
None => println!("未找到"),
}
// 方法 2:if let(只关心一种情况)
if let Some(name) = find_user(2) {
println!("{}", name);
}
// 方法 3:组合子方法
let name = find_user(1)
.unwrap_or(String::from("默认用户"));
}
Option 常用方法
let x: Option<i32> = Some(42);
// 取值
x.unwrap(); // 42(None 时 panic)
x.expect("空值!"); // 42(None 时 panic 并附消息)
x.unwrap_or(0); // 42(None 时返回默认值)
x.unwrap_or_default(); // 42(None 时返回类型默认值)
x.unwrap_or_else(|| expensive_default()); // 惰性默认值
// 转换
x.map(|v| v * 2); // Some(84)
x.and_then(|v| if v > 0 { Some(v) } else { None }); // Some(42)
x.filter(|&v| v > 100); // None
x.or(Some(0)); // Some(42)(x 有值时返回 x)
x.zip(Some("hello")); // Some((42, "hello"))
// 检查
x.is_some(); // true
x.is_none(); // false
Result — 可恢复的错误处理
enum Result<T, E> {
Ok(T), // 成功
Err(E), // 失败
}
use std::fs;
use std::io;
fn read_config() -> Result<String, io::Error> {
let content = fs::read_to_string("config.toml")?; // ? 自动传播错误
Ok(content)
}
fn main() {
match read_config() {
Ok(content) => println!("配置: {}", content),
Err(e) => eprintln!("读取失败: {}", e),
}
}
详细内容请阅读 错误处理
枚举的大小
枚举的大小 = 最大变体的大小 + 判别符(discriminant):
use std::mem::size_of;
enum Small {
A, // 0 字节数据
B(u8), // 1 字节数据
}
// size_of::<Small>() = 1 (判别符) + 1 (数据) = 2
enum Large {
A,
B([u8; 1024]), // 1024 字节数据
}
// size_of::<Large>() ≈ 1 + 1024 = 1025(加上对齐可能更大)
空间优化(Niche Optimization)
Rust 编译器对 Option<&T>、Option<Box<T>> 等类型做了空间优化——利用引用/指针不可能为 0 的特性,用 0 表示 None,不需要额外的判别符:
assert_eq!(size_of::<Option<&i32>>(), size_of::<&i32>()); // 8 = 8,无额外开销!
assert_eq!(size_of::<Option<Box<i32>>>(), size_of::<Box<i32>>()); // 8 = 8
常见面试问题
Q1: Rust 的 enum 和 C/Java 的 enum 有什么区别?
答案:
| 特性 | C/Java enum | Rust enum |
|---|---|---|
| 变体数据 | 只是整数标签 | 每个变体可携带不同类型数据 |
| 类型安全 | C 的 enum 本质是整数 | Rust 是独立类型 |
| 模式匹配 | 需要 if/switch | match 强制穷尽检查 |
| 方法 | Java 可以,C 不行 | 可以 impl 方法 |
| 泛型 | Java 不支持 | 支持(如 Option<T>) |
Rust 的 enum 本质是代数数据类型(ADT)中的和类型(Sum Type),远比传统枚举强大。
Q2: Option<T> 相比 null 有什么优势?
答案:
- 编译期安全:
Option<T>和T是不同类型,不能混用。必须显式处理None才能获取值 - 穷尽检查:
match强制你处理所有情况,不会遗漏 - 零开销:
Option<&T>和&T大小相同(Niche Optimization) - 表达力:函数签名明确告诉调用者"这个值可能不存在"
Q3: &self、&mut self、self 作为方法参数有什么区别?
答案:
| 签名 | 含义 | 调用后 |
|---|---|---|
&self | 不可变借用 | 实例仍可使用 |
&mut self | 可变借用 | 实例仍可使用(需 mut) |
self | 获取所有权 | 实例被消耗,不可再用 |
mut self | 获取所有权 + 内部可改 | 实例被消耗 |
选择原则:
- 只读操作 →
&self - 需要修改 →
&mut self - 需要转换/消耗自身 →
self(如into_xxx()方法)
Q4: 结构体更新语法 ..other 会 Move 还是 Copy?
答案:
取决于字段的类型:
Copy类型的字段 → 复制- 非
Copy类型的字段 → Move
let u1 = User { name: String::from("A"), email: String::from("a@b"), age: 30, active: true };
let u2 = User { name: String::from("B"), ..u1 };
// u1.email 被 move(String 不是 Copy)
// u1.age 和 u1.active 被 copy(i32, bool 是 Copy)
println!("{}", u1.age); // ✅
// println!("{}", u1.email); // ❌ 已 move
Q5: 什么是 Newtype 模式?
答案:
用单字段元组结构体包装已有类型,创建语义不同的新类型:
struct Meters(f64);
struct Seconds(f64);
fn speed(distance: Meters, time: Seconds) -> f64 {
distance.0 / time.0
}
// speed(Seconds(10.0), Meters(100.0)) // ❌ 编译错误:参数顺序错了!
优势:
- 类型安全:防止参数混淆
- 绕过孤儿规则:可以为外部类型实现外部 trait
- 零成本:编译后和裸类型完全一样
更多内容请参阅 Newtype 模式
Q6: 什么是 Niche Optimization(空间优化)?
答案:
编译器利用某些类型的"不可能值"来存储枚举的判别符,从而节省空间。例如:
&T永远不为 null →Option<&T>用 null 指针表示NoneNonZeroU32不为 0 →Option<NonZeroU32>用 0 表示Nonebool只有 0 和 1 →Option<bool>用 2 表示None
结果:Option<&T> 和 &T 大小完全一样(8 字节),完全零开销。