结构体与枚举:Rust中的自定义数据类型
说实话,刚接触Rust的时候,我觉得结构体和枚举跟其他语言差不多。但用久了才发现,Rust在这块的设计是真的讲究。今天咱们就来聊聊这两个核心概念。
结构体:把数据组织起来
结构体说白了就是把你关心的数据打包在一起。我习惯把它想象成一个表格,每一列就是一个字段。
struct User {
username: String,
email: String,
sign_in_count: u64,
active: bool,
}
定义好之后怎么用?实例化呗:
let user1 = User {
email: String::from("someone@example.com"),
username: String::from("someusername123"),
active: true,
sign_in_count: 1,
};
这里有个小细节——字段的顺序可以随意,不像C语言那样必须按声明顺序来。我个人觉得这设计挺人性化的。
let user2 = User {
email: String::from("another@example.com"),
username: String::from("anotherusername567"),
..user1
};
注意这里的..user1会把剩余字段的值搬过来。但有个坑——如果字段实现了Copy trait,那原变量还能用;否则所有权就转移了。
元组结构体:有名字的元组
有时候你不想给每个字段起名字,但又想区分不同类型。元组结构体就是干这个的。
struct Color(i32, i32, i32);
struct Point(i32, i32, i32);
let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);
你看,Color和Point内部都是三个i32,但它们是不同的类型。我在项目中用这个来区分不同单位的数值,比如米和厘米,避免搞混。
方法:让结构体动起来
光有数据不够,还得有行为。Rust用impl块给结构体添加方法。
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
}
调用方法很简单:rect.area()。注意&self是self: &Self的简写,表示不可变借用。如果你要修改实例,就用&mut self。
width,又定义了个方法也叫width,那rect.width返回的是方法结果,不是字段值。这设计其实是为了实现getter模式。
枚举:有限的可能性
枚举表示「要么是A,要么是B,要么是C」。Rust的枚举比C语言的强大多了——每个变体可以携带不同类型的数据。
enum IpAddr {
V4(String),
V6(String),
}
let home = IpAddr::V4(String::from("127.0.0.1"));
let loopback = IpAddr::V6(String::from("::1"));
你想想看,Java里要表达这种「带数据的枚举」,得搞个类层次结构。Rust一行搞定。
Option和Result:Rust的左右护法
这两个枚举太重要了,以至于它们被预置到了标准库的prelude里。
Option:表示「可能有值,也可能没有」。
enum Option<T> {
Some(T),
None,
}
为什么不用null?因为null是十亿美元的错误。Rust用Option强制你处理空值情况。我刚开始觉得麻烦,后来发现这习惯救了我无数次。
Result:表示「要么成功,要么失败」。
enum Result<T, E> {
Ok(T),
Err(E),
}
文件读取、网络请求、JSON解析……几乎所有可能失败的操作都返回Result。你没法忽略错误处理,编译器会提醒你。
match模式匹配:优雅的条件分支
match是Rust的瑞士军刀。它不像C语言的switch只能匹配整数,match能匹配任何模式。
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
None => None,
Some(i) => Some(i + 1),
}
}
这里有个关键点——match必须穷举所有可能性。如果你只写了Some(i)忘了None,编译器会报错。这设计就是为了防止你漏掉边界情况。
if let:偷懒的match
有时候你只关心某一种模式,其他情况一概忽略。这时候用if let比match简洁得多。
let config_max = Some(3u8);
match config_max {
Some(max) => println!("The maximum is configured to be {}", max),
_ => (),
}
// 等价于
if let Some(max) = config_max {
println!("The maximum is configured to be {}", max);
}
我个人习惯在只处理一种情况时用if let,需要处理多种情况时用match。没有绝对的好坏,看场景。
知识体系总览
下面这张图把本章的核心概念串起来了:
总结一下
结构体和枚举是Rust类型系统的基石。结构体帮你把数据组织得井井有条,枚举让你优雅地表达「或」的关系。配合match和if let,你能写出既安全又清晰的代码。
嗯,这里要注意一点——别一上来就想用最复杂的特性。我见过不少新手把枚举嵌套得特别深,结果自己都看不懂。先从简单的结构体开始,需要时再引入枚举,这样代码更容易维护。