跳到主要内容

结构体与枚举

问题

Rust 的结构体和枚举有什么特点?OptionResult 的设计原理是什么?

答案

结构体(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 enumRust enum
变体数据只是整数标签每个变体可携带不同类型数据
类型安全C 的 enum 本质是整数Rust 是独立类型
模式匹配需要 if/switchmatch 强制穷尽检查
方法Java 可以,C 不行可以 impl 方法
泛型Java 不支持支持(如 Option<T>

Rust 的 enum 本质是代数数据类型(ADT)中的和类型(Sum Type),远比传统枚举强大。

Q2: Option<T> 相比 null 有什么优势?

答案

  1. 编译期安全Option<T>T 是不同类型,不能混用。必须显式处理 None 才能获取值
  2. 穷尽检查match 强制你处理所有情况,不会遗漏
  3. 零开销Option<&T>&T 大小相同(Niche Optimization)
  4. 表达力:函数签名明确告诉调用者"这个值可能不存在"

Q3: &self&mut selfself 作为方法参数有什么区别?

答案

签名含义调用后
&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 指针表示 None
  • NonZeroU32 不为 0 → Option<NonZeroU32> 用 0 表示 None
  • bool 只有 0 和 1 → Option<bool> 用 2 表示 None

结果:Option<&T>&T 大小完全一样(8 字节),完全零开销。

相关链接