跳到主要内容

结构体和枚举

结构体(Struct)和枚举(Enum)是 Rust 中创建自定义数据类型的两种核心方式。结构体用于组合相关数据,枚举用于表示一组可能的值。

结构体

结构体是一种自定义数据类型,用于将多个相关的值组合成一个有意义的整体。

定义结构体

使用 struct 关键字定义结构体:

// 定义一个用户结构体
struct User {
username: String,
email: String,
active: bool,
sign_in_count: u64,
}

解释

  • struct 关键字后面是结构体名称
  • 大括号内定义字段(fields),每个字段有名称和类型
  • 字段之间用逗号分隔

创建结构体实例

fn main() {
// 创建实例
let user1 = User {
email: String::from("[email protected]"),
username: String::from("user123"),
active: true,
sign_in_count: 1,
};

// 访问字段(使用点号)
println!("邮箱: {}", user1.email);
println!("用户名: {}", user1.username);
}

注意:创建实例时,字段顺序不需要与定义顺序一致。

可变实例

结构体实例默认不可变,要修改字段需要声明为可变:

fn main() {
let mut user1 = User {
email: String::from("[email protected]"),
username: String::from("user123"),
active: true,
sign_in_count: 1,
};

// 修改字段
user1.email = String::from("[email protected]");
user1.sign_in_count += 1;
}

重要:Rust 不允许将部分字段标记为可变,整个实例必须是可变的。

字段初始化简写

当变量名与字段名相同时,可以使用简写语法:

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

结构体更新语法

从现有实例创建新实例时,可以使用 .. 语法复用字段:

fn main() {
let user1 = User {
email: String::from("[email protected]"),
username: String::from("user123"),
active: true,
sign_in_count: 1,
};

// 使用 user1 的部分字段创建 user2
let user2 = User {
email: String::from("[email protected]"),
..user1 // 其余字段来自 user1
};

// 注意:user1 的 username 已移动到 user2
// println!("{}", user1.username); // 错误!
println!("{}", user1.active); // 正确,bool 是 Copy 类型
}

解释

  • ..user1 必须放在最后
  • 对于实现了 Copy trait 的类型,值会被复制
  • 对于拥有所有权的类型(如 String),所有权会被转移

元组结构体

不需要字段名时,可以使用元组结构体:

// 定义元组结构体
struct Color(i32, i32, i32);
struct Point(i32, i32, i32);

fn main() {
let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);

// 访问元素
println!("红色分量: {}", black.0);

// 解构
let Color(r, g, b) = black;
println!("RGB({}, {}, {})", r, g, b);

// black 和 origin 是不同类型
// let mixed: Color = origin; // 错误!类型不匹配
}

用途

  • 给整个元组一个有意义的名称
  • 区分相同结构但语义不同的类型

单元结构体

没有字段的结构体,称为单元结构体:

// 单元结构体
struct AlwaysEqual;
struct Marker;

fn main() {
let subject = AlwaysEqual;
let marker = Marker;

// 常用于实现 trait 但不需要存储数据
}

用途

  • 实现某个 trait 但不需要存储数据
  • 类型标记(type marker)
  • 状态机中的状态标记

结构体方法

使用 impl 块为结构体定义方法:

#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}

impl Rectangle {
// 方法:第一个参数是 self
fn area(&self) -> u32 {
self.width * self.height
}

// 方法:可以修改 self
fn scale(&mut self, factor: u32) {
self.width *= factor;
self.height *= factor;
}

// 关联函数:没有 self 参数
fn square(size: u32) -> Self {
Self {
width: size,
height: size,
}
}

// 带多个参数的方法
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
}

fn main() {
let mut rect = Rectangle {
width: 30,
height: 50,
};

// 调用方法
println!("面积: {}", rect.area());

rect.scale(2);
println!("缩放后: {:?}", rect);

// 调用关联函数(使用 :: 语法)
let square = Rectangle::square(10);
println!("正方形: {:?}", square);

// 方法带参数
let rect2 = Rectangle { width: 10, height: 40 };
println!("rect 能容纳 rect2: {}", rect.can_hold(&rect2));
}

self 的三种形式

形式说明使用场景
&self不可变借用只读取数据
&mut self可变借用需要修改数据
self获取所有权转换或消耗实例

自动引用和解引用

Rust 会自动为方法调用添加引用或解引用:

fn main() {
let rect = Rectangle { width: 30, height: 50 };

// 以下两种写法等价
rect.area(); // 自动添加 &
(&rect).area(); // 显式写法

let p = &Rectangle { width: 10, height: 20 };
p.area(); // 自动解引用
}

多个 impl 块

Rust 允许为同一类型定义多个 impl 块:

impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
}

impl Rectangle {
fn perimeter(&self) -> u32 {
2 * (self.width + self.height)
}
}

枚举

枚举(Enum)允许定义一个类型,它可以是几个不同变体中的一个。

定义枚举

使用 enum 关键字定义枚举:

// 简单枚举
enum IpAddrKind {
V4,
V6,
}

fn main() {
let four = IpAddrKind::V4;
let six = IpAddrKind::V6;

// 枚举变体在枚举命名空间下
route(IpAddrKind::V4);
route(IpAddrKind::V6);
}

fn route(ip_kind: IpAddrKind) {
match ip_kind {
IpAddrKind::V4 => println!("IPv4 地址"),
IpAddrKind::V6 => println!("IPv6 地址"),
}
}

带数据的枚举

枚举变体可以携带数据:

// 带数据的枚举
enum IpAddr {
V4(u8, u8, u8, u8),
V6(String),
}

fn main() {
let home = IpAddr::V4(127, 0, 0, 1);
let loopback = IpAddr::V6(String::from("::1"));

match home {
IpAddr::V4(a, b, c, d) => {
println!("IPv4: {}.{}.{}.{}", a, b, c, d);
}
IpAddr::V6(addr) => {
println!("IPv6: {}", addr);
}
}
}

复杂数据结构

枚举变体可以包含不同类型的数据:

enum Message {
Quit, // 没有数据
Move { x: i32, y: i32 }, // 命名字段
Write(String), // 字符串
ChangeColor(i32, i32, i32), // 三个整数
}

fn main() {
let msg1 = Message::Quit;
let msg2 = Message::Move { x: 10, y: 20 };
let msg3 = Message::Write(String::from("Hello"));
let msg4 = Message::ChangeColor(255, 0, 0);

process_message(msg1);
process_message(msg2);
}

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!("颜色: RGB({}, {}, {})", r, g, b);
}
}
}

解释

  • 每个变体可以携带不同类型和数量的数据
  • 使用花括号可以给数据命名
  • 枚举将所有变体统一为一个类型

枚举方法

枚举也可以使用 impl 定义方法:

impl Message {
fn call(&self) {
match self {
Message::Quit => println!("退出"),
Message::Move { x, y } => println!("移动: ({}, {})", x, y),
Message::Write(s) => println!("写入: {}", s),
Message::ChangeColor(r, g, b) => println!("颜色: {},{},{}", r, g, b),
}
}

fn is_quit(&self) -> bool {
matches!(self, Message::Quit)
}
}

fn main() {
let msg = Message::Write(String::from("Hello"));
msg.call();
println!("是否退出: {}", msg.is_quit());
}

Option 枚举

Option 是标准库中最重要的枚举之一,用于表示可能存在的值:

// 标准库定义
enum Option<T> {
Some(T),
None,
}
fn main() {
// Some 包含值
let some_number = Some(5);
let some_string = Some("字符串");

// None 表示空值
let absent_number: Option<i32> = None;

// 使用 match 处理 Option
let x = Some(5);
match x {
Some(i) => println!("值是: {}", i),
None => println!("没有值"),
}

// if let 简化处理
if let Some(i) = x {
println!("值是: {}", i);
}
}

为什么 Option 比 null 更好?

fn main() {
let x: i8 = 5;
let y: Option<i8> = Some(10);

// 编译错误!无法直接相加
// let sum = x + y;

// 必须显式处理 None 的情况
let sum = match y {
Some(v) => x + v,
None => x,
};
println!("和: {}", sum);
}

解释

  • Option<T>T 是不同的类型
  • 编译器强制你处理值为空的情况
  • 避免了"空指针异常"这类常见错误

Result 枚举

Result 用于表示可能失败的操作:

// 标准库定义
enum Result<T, E> {
Ok(T), // 成功,包含值
Err(E), // 失败,包含错误
}
use std::fs::File;
use std::io::ErrorKind;

fn main() {
// 打开文件可能失败
let f = File::open("hello.txt");

let f = match f {
Ok(file) => file,
Err(error) => match error.kind() {
ErrorKind::NotFound => {
// 文件不存在,创建新文件
File::create("hello.txt").unwrap()
}
other_error => {
panic!("打开文件失败: {:?}", other_error);
}
},
};
}

unwrap 和 expect

简化 ResultOption 的处理:

use std::fs::File;

fn main() {
// unwrap:成功返回值,失败 panic
let f = File::open("hello.txt").unwrap();

// expect:可以自定义错误信息
let f = File::open("hello.txt")
.expect("无法打开 hello.txt");
}

注意:生产代码中应避免使用 unwrapexpect,改用更完善的错误处理。

? 运算符

? 运算符用于简化错误传播:

use std::fs::File;
use std::io::{self, Read};

// 传统方式
fn read_username_from_file() -> Result<String, io::Error> {
let f = File::open("hello.txt");

let mut f = match f {
Ok(file) => file,
Err(e) => return Err(e),
};

let mut s = String::new();

match f.read_to_string(&mut s) {
Ok(_) => Ok(s),
Err(e) => Err(e),
}
}

// 使用 ? 运算符简化
fn read_username_from_file_simple() -> Result<String, io::Error> {
let mut f = File::open("hello.txt")?;
let mut s = String::new();
f.read_to_string(&mut s)?;
Ok(s)
}

// 更简洁的写法
fn read_username_short() -> Result<String, io::Error> {
std::fs::read_to_string("hello.txt")
}

解释

  • 如果 ResultOk,返回其中的值
  • 如果 ResultErr,立即返回错误
  • 只能在返回 Result 的函数中使用

模式匹配

match 表达式

match 用于匹配枚举的所有可能变体:

enum Coin {
Penny,
Nickel,
Dime,
Quarter(UsState),
}

#[derive(Debug)]
enum UsState {
Alabama,
Alaska,
}

fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter(state) => {
println!("州25美分: {:?}", state);
25
}
}
}

fn main() {
let coin = Coin::Quarter(UsState::Alaska);
println!("价值: {} 美分", value_in_cents(coin));
}

匹配规则

  • 必须覆盖所有可能的情况
  • _ 是通配符,匹配任何值
  • 匹配分支可以绑定值

匹配 Option

fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
Some(i) => Some(i + 1),
None => None,
}
}

fn main() {
let five = Some(5);
let six = plus_one(five);
let none = plus_one(None);

println!("{:?}, {:?}", six, none);
}

匹配范围

fn main() {
let number = 13;

match number {
1 => println!("一"),
2 | 3 | 5 | 7 => println!("质数"),
13..=19 => println!("十几岁"),
_ => println!("其他"),
}
}

if let 语法

当只关心一种情况时,if let 更简洁:

fn main() {
let some_value = Some(3);

// 使用 match
match some_value {
Some(3) => println!("三"),
_ => (),
}

// 使用 if let(更简洁)
if let Some(3) = some_value {
println!("三");
}

// 可以加 else
if let Some(x) = some_value {
println!("值是 {}", x);
} else {
println!("没有值");
}
}

while let 语法

持续匹配直到失败:

fn main() {
let mut stack = Vec::new();
stack.push(1);
stack.push(2);
stack.push(3);

// 当 pop 返回 Some 时继续循环
while let Some(top) = stack.pop() {
println!("{}", top);
}
}

派生 Trait

使用 #[derive] 属性自动实现常用 trait:

#[derive(Debug, Clone, PartialEq)]
struct Point {
x: i32,
y: i32,
}

fn main() {
let p1 = Point { x: 1, y: 2 };

// Debug:打印调试信息
println!("{:?}", p1);

// Clone:克隆
let p2 = p1.clone();

// PartialEq:比较相等
println!("p1 == p2: {}", p1 == p2);
}

常用派生 Trait

Trait功能
Debug打印调试信息 {:?}
Clone显式克隆
Copy隐式复制
PartialEq比较 ==!=
Eq完全相等
PartialOrd比较 <, >, <=, >=
Ord排序
Hash哈希

小结

本章我们学习了:

  1. 结构体:定义、实例化、方法、关联函数
  2. 枚举:定义、带数据的变体、Option、Result
  3. 模式匹配:match、if let、while let
  4. 错误处理:unwrap、expect、? 运算符
  5. 派生 Trait:自动实现常用功能

练习

  1. 定义一个 Book 结构体,包含书名、作者、页数、价格,并实现方法
  2. 定义一个 TrafficLight 枚举,实现返回每种灯持续时间的方法
  3. 使用 Option 实现一个安全的除法函数
  4. 实现一个函数,从 Vec<Option<i32>> 中提取所有 Some