结构体与枚举: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);

你看,ColorPoint内部都是三个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()。注意&selfself: &Self的简写,表示不可变借用。如果你要修改实例,就用&mut self

我曾经踩过的坑:方法名和字段名重名时,Rust会优先调用方法。比如你有个字段叫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,编译器会报错。这设计就是为了防止你漏掉边界情况。

实战经验:我在写网络协议解析时,match配合枚举简直无敌。每个协议包类型对应一个枚举变体,match分支处理不同包。代码清晰,扩展也方便。

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。没有绝对的好坏,看场景。

知识体系总览

下面这张图把本章的核心概念串起来了:

结构体与枚举知识体系 结构体 (struct) 普通结构体 元组结构体 impl 块 → 方法 枚举 (enum) 自定义枚举 Option/Result 模式匹配 match(穷举) if let(单模式) 结构体组织数据,枚举定义可能性,match/if let 处理逻辑 核心思想:用类型系统表达业务逻辑,让编译器帮你检查错误

总结一下

结构体和枚举是Rust类型系统的基石。结构体帮你把数据组织得井井有条,枚举让你优雅地表达「或」的关系。配合match和if let,你能写出既安全又清晰的代码。

嗯,这里要注意一点——别一上来就想用最复杂的特性。我见过不少新手把枚举嵌套得特别深,结果自己都看不懂。先从简单的结构体开始,需要时再引入枚举,这样代码更容易维护。


专注资料整理