结构体和枚举
结构体(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必须放在最后- 对于实现了
Copytrait 的类型,值会被复制 - 对于拥有所有权的类型(如
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
简化 Result 和 Option 的处理:
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");
}
注意:生产代码中应避免使用 unwrap 和 expect,改用更完善的错误处理。
? 运算符
? 运算符用于简化错误传播:
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")
}
解释:
- 如果
Result是Ok,返回其中的值 - 如果
Result是Err,立即返回错误 - 只能在返回
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 | 哈希 |
小结
本章我们学习了:
- 结构体:定义、实例化、方法、关联函数
- 枚举:定义、带数据的变体、Option、Result
- 模式匹配:match、if let、while let
- 错误处理:unwrap、expect、? 运算符
- 派生 Trait:自动实现常用功能
练习
- 定义一个
Book结构体,包含书名、作者、页数、价格,并实现方法 - 定义一个
TrafficLight枚举,实现返回每种灯持续时间的方法 - 使用
Option实现一个安全的除法函数 - 实现一个函数,从
Vec<Option<i32>>中提取所有Some值