Rust 结构体、枚举与模式匹配

要点

struct 表达同时存在的字段,enum 表达互斥的状态。把状态建模进类型,可以避免多个布尔值组合出无效情况。

目录


核心概念

概念一:结构体 —— 数据的组织单元

定义: struct 把若干相关字段组织成一个自定义类型,impl 块为其附加关联函数与方法,角色类似其他语言的 class(但没有继承)。

三种形态: – 具名字段结构体: struct App { name: String, stars: u32 }——绝大多数场景用这个 – 元组结构体: struct Point(f64, f64)——字段少且不需要名字时 – 单元结构体: struct Marker;——只作为类型标记,不存数据

关键特性: – 方法第一参数是所有权视角:&self 只读、&mut self 可写、self 消费自身(正是 02 篇的借用规则) – 无 new 关键字:惯例用一个无 self 的关联函数充当构造器 – 字段默认私有,跨模块暴露需要 pub(工程化内容后续篇目展开)

使用场景: – 场景一:领域实体(用户、订单、配置项)的数据载体 – 场景二:为第三方数据源(JSON/API)定义映射结构

概念二:枚举 —— 有限可能性的建模

定义: enum 表示”值只能是若干变体之一”,且每个变体可以携带不同类型的数据。类型系统中称这类结构为”和类型”(sum type)。

关键特性: – 变体携带数据的能力远强于 C 风格 enum:Mobile { os: String }、Web(String) 都合法 – 标准库两大核心类型 Option<T> 与 Result<T, E> 就是普通枚举——学会 enum 即学会错误处理的骨架 – 枚举同样可以实现方法、参与泛型

使用场景: – 场景一:状态机(连接状态、订单状态)——每个状态携带各自的数据 – 场景二:消息协议(如本模块 Tauri IPC 的事件类型)——一个枚举穷举所有事件

概念三:Option —— “没有值”的一等公民

定义: Option<T> 是标准库枚举,只有 Some(T) 与 None 两个变体,显式表达”可能有值也可能没有”。

关键特性: – Rust 没有 null:空缺被建模为类型,编译器强制你在用值之前处理 None – 取值四件套:match(全覆盖)、if let(只关心有值)、unwrap_or(兜底默认值)、let else(无值提前退出) – 彻底消除”空引用调用”这类运行时崩溃(即所谓”十亿美元错误”)

概念四:match —— 穷尽性检查的表达力

定义: match 将一个值与一系列模式比较并执行首个命中的分支,编译器强制所有可能情况都被覆盖。

关键特性: – 穷尽性: 枚举每新增一个变体,所有未覆盖它的 match 都会编译报错——重构免费的完整性保障 – 解构: 模式可以直接拆开变体携带的数据(Move { x, y }) – 匹配守卫: 分支后追加 if 条件做进一步过滤 – 必须返回同一类型——match 本身是表达式,有值


实践指南

步骤一:建模一个跨平台应用的目标平台

目标: 用枚举完成一次真实的领域建模(呼应本模块 Tauri 2 跨端方向)。

操作指南: 1. cargo new platform-model && cd platform-model 2. 定义 Platform 枚举(可直接采用下方示例二的定义) 3. 实现 name(&self) -> &str 方法,用 match 覆盖全部变体 4. 新增一个变体(比如 Watch),先不更新 match——观察编译器如何指出每一个遗漏的位置 5. 补全分支直到编译通过

验证方法: cargo run 输出所有目标平台名;故意漏分支时能读到编译器的穷尽性报错

步骤二:用 Option 重构一次”返回 -1 表示找不到”

目标: 把哨兵值风格(返回 -1/null)改成 Option 风格。

操作指南: 1. 写一个在数组中查找目标的函数,初版返回 i32,找不到返回 -1 2. 改签名为 fn find(numbers: &[i32], target: i32) -> Option<i32> 3. 在调用侧分别用 match、unwrap_or 处理,感受”空值是类型而非魔法数”

验证方法: 调用侧不存在任何与 -1 比较的代码


代码示例

以下四段是可用 rustc --edition 2024 复核的完整示例。请在自己的工具链中编译、运行,并记录实际版本和输出。

示例一:结构体与 impl

// 结构体:把相关数据组织在一起
struct App {
    name: String,
    version: String,
    stars: u32,
}

// impl 块为结构体实现关联函数与方法(类似其他语言的类方法)
impl App {
    // 关联函数(没有 self),通常作为构造器
    fn new(name: &str, version: &str) -> Self {
        Self {
            name: name.to_string(),
            version: version.to_string(),
            stars: 0,
        }
    }

    // 方法:&self 只读借用
    fn describe(&self) -> String {
        format!("{} v{} ({} stars)", self.name, self.version, self.stars)
    }

    // 方法:&mut self 可变借用
    fn add_star(&mut self) {
        self.stars += 1;
    }
}

fn main() {
    let mut app = App::new("dev-quest", "0.1.0");
    app.add_star();
    app.add_star();
    println!("{}", app.describe());
}

关键点解析: – new 是关联函数:通过 App::new(...) 调用,无 self 参数 – 三个方法的 self 形态分别演示了”构造返回 Self / 只读 / 可写”三种所有权视角 – 字段接 String(拥有数据)而构造器接 &str(借用传入),是 02 篇”参数优先借用”惯例的直接应用

示例二:枚举与穷尽 match

// 枚举:值只能是有限变体之一,每个变体可携带不同数据
enum Platform {
    Windows,
    MacOS,
    Linux,
    Mobile { os: String }, // 类结构体变体
    Web(String),           // 类元组变体
}

// 枚举上同样可以实现方法
impl Platform {
    fn name(&self) -> &str {
        match self {
            Platform::Windows => "Windows",
            Platform::MacOS => "macOS",
            Platform::Linux => "Linux",
            Platform::Mobile { os } => os,
            Platform::Web(framework) => framework,
        }
    }
}

fn main() {
    let targets = [
        Platform::Windows,
        Platform::MacOS,
        Platform::Linux,
        Platform::Mobile { os: String::from("iOS") },
        Platform::Web(String::from("wasm")),
    ];

    for p in &targets {
        println!("目标平台: {}", p.name());
    }
}

关键点解析: – 三种变体形态同框:无数据、类结构体(命名字段)、类元组(带一个值) – match 直接解构变体数据:os、framework 都是新绑定 – 删掉任何一个分支试试——编译器会报”非穷尽匹配”,这就是枚举建模的安全性来源

示例三:Option 的四种处理方式

// 在列表中查找:Rust 用 Option 表达"可能有值也可能没有"
fn find_even(numbers: &[i32]) -> Option<i32> {
    for &n in numbers {
        if n % 2 == 0 {
            return Some(n);
        }
    }
    None
}

fn main() {
    // 方式一:match 全覆盖处理两种情况
    match find_even(&[1, 3, 5]) {
        Some(n) => println!("找到偶数: {n}"),
        None => println!("列表中没有偶数"),
    }

    // 方式二:if let 只关心"有值"的情况
    if let Some(n) = find_even(&[2, 4]) {
        println!("第一个偶数: {n}");
    }

    // 方式三:unwrap_or 提供默认值兜底
    let n = find_even(&[1, 3]).unwrap_or(-1);
    println!("默认值兜底: {n}");

    // 方式四:let else —— 没有值就提前退出当前函数
    let Some(n) = find_even(&[8]) else {
        println!("没有偶数,直接结束");
        return;
    };
    println!("let else 拿到: {n}");
}

关键点解析: – 返回 Option<i32> 把”可能找不到”写进类型签名,调用方无法假装没看见 – 四种取值方式按场景选用:全覆盖用 match、单分支用 if let / let else、有默认值用 unwrap_or – find_even(&[1, 3, 5]) 直接传数组字面量:参数是 &[i32](切片),比 &Vec<i32> 更通用

示例四:match 的高级模式

enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(u8, u8, u8),
}

fn process(msg: &Message) -> String {
    match msg {
        // 单元变体
        Message::Quit => String::from("退出程序"),
        // 解构命名字段 + 匹配守卫
        Message::Move { x, y } if *x == 0 && *y == 0 => String::from("原地不动"),
        Message::Move { x, y } => format!("移动到 ({x}, {y})"),
        // 解构类元组变体
        Message::Write(text) => format!("写入: {text}"),
        Message::ChangeColor(r, g, b) => format!("调色 rgb({r},{g},{b})"),
    }
}

fn main() {
    let msgs = [
        Message::Quit,
        Message::Move { x: 0, y: 0 },
        Message::Move { x: 10, y: -3 },
        Message::Write(String::from("你好")),
        Message::ChangeColor(255, 128, 0),
    ];

    for m in &msgs {
        println!("{}", process(m));
    }
}

关键点解析: – 同一变体可以出现多个分支:先匹配更特殊的(守卫 if *x == 0 && *y == 0),再匹配一般的 – msg 是 &Message,模式中的绑定自动成为引用(*x 解引用)——匹配借用值不需要额外动作 – 顺序敏感:守卫分支必须在通配的 Move 分支之前


最佳实践

enum 表达互斥状态,可避免 loading 与 failed 同时为真;多个可独立成立的事实仍可用多个布尔值。match 的分支结果需要能统一到兼容类型,某些发散分支如提前返回有不同推断行为,不能只按表面写法判断。

对可能缺失的外部数据显式处理 None/Err;unwrap/expect 只在不变量确实保证成功时使用,并说明理由。derive(Debug) 有助调试,但含敏感字段时检查输出。新增变体后编译并测试所有状态转换,不用通配分支无声忽略新情况。

正文提取验证:枚举数据与穷尽匹配

该单文件程序为每个变体提供一个分支并读取变体携带的数据。Message 新增变体时,describe 将因不穷尽而无法编译,除非同步更新分支。

enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
}

fn describe(message: &Message) -> String {
    match message {
        Message::Quit => "quit".to_string(),
        Message::Move { x, y } => format!("move:{x},{y}"),
        Message::Write(text) => format!("write:{text}"),
    }
}

fn main() {
    let messages = [
        Message::Quit,
        Message::Move { x: 2, y: -1 },
        Message::Write("hi".to_string()),
    ];
    for message in &messages {
        println!("{}", describe(message));
    }
}

预期输出为 quit、move:2,-1、write:hi。运行不覆盖借用复杂场景、匹配守卫或跨线程传递。


常见问题

Q1: match 与 if/else 的本质区别?

A: ① match 有穷尽性检查,漏情况编译不过,if/else 没有;② match 能解构数据(把变体里的字段直接绑定为新变量);③ match 是表达式有返回值。凡是”按值的形态分发”的场景,match 都是更安全的选择。

Q2: 枚举变体各自携带不同类型的数据,会不会很乱?

A: 恰恰相反——这正是 Rust 枚举强于传统 enum 的地方。每个变体是独立的”小结构体”,match 解构后各自拿到正确类型的绑定,编译器全程把关。消息协议、状态机、AST 节点都是典型受益场景。

Q3: match 匹配 &Message 时为什么模式里不用写 &?

A: 匹配引用时模式会自动”穿透”引用进行绑定(绑定成为引用本身),因此示例中 *x 需要解引用后比较。这种默认行为让多数 match 代码保持简洁;需要所有权转移的复杂场景会在后续篇目中展开。


交叉引用

本模块

  • 📄 02 所有权与借用 — 方法接收器 &self / &mut self / self 的规则来源
  • 📄 04 trait 与泛型 — 给自定义类型附加共享行为
  • 📄 05 错误处理 — Result<T, E> 就是一个带两个变体的枚举
  • 📖 模块 README — 本篇在入门路径 01→05 中的位置

总结

核心要点回顾

  1. struct + impl: 具名字段结构体是默认选择,构造器是惯例而非关键字
  2. enum 是和类型: 变体可携带数据,Option/Result 都由它构成
  3. match 的穷尽性: 漏分支编译不过,新增变体的重构成本被编译器兜底
  4. 没有 null: “没有值”是 Option<T>,四种取值方式按场景选用

学习成果检查

  • [ ] 能说出三种结构体形态及适用场景
  • [ ] 能用枚举为一个业务状态建模并穷尽处理
  • [ ] 四种 Option 取值方式信手拈来且知道各自适用场景
  • [ ] 理解”穷尽性检查是免费的完整性保障”这句话的含义

最后更新: 2026年9月

🎯 下一步: 类型会定义了,接着学习 04 trait 与泛型——让不同的类型共享同一套行为契约。

暂无评论

发送评论 编辑评论

|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇