【AI开发之Rust】第 6 课:结构体、枚举与方法 —— 开始定义自己的类型

第 6 课:结构体、枚举与方法 ------ 开始定义自己的类型

预计时间:75-90 分钟 | 难度:⭐⭐ | 前置:第 4-5 课(所有权与生命周期)


6.1 这节课解决什么问题

前几课一直在用语言内置的类型(数字、字符串、数组)。从本课起,你要学会为自己的业务造类型------课程实战里的"用户、会话、消息、API 请求/响应"全都得靠本课知识建模。

本课会掌握四件武器:

复制代码
struct  结构体  ------ 把相关字段打包(类似 JS 对象 / Python class 的"字段集合")
enum    枚举    ------ 表示"是 A、B 或 C"的互斥状态(这是 Rust 比大多数语言强的地方)
impl    方法    ------ 给类型挂行为(相当于"类的方法")
match   匹配    ------ 拆开并穷尽地处理 enum 的所有可能

另外你会认识 Rust 里出镜率最高的两个内置枚举 Option<T>Result<T, E>------理解它们,等于理解半个 Rust 错误处理体系。

6.2 结构体 struct:打包相关数据

6.2.1 定义、构造与访问

rust 复制代码
// 字段命名惯例:snake_case
struct User {
    username: String,        // 拥有自己的数据(不是引用,见 5.6 的建议)
    email: String,
    age: u8,
    active: bool,
}

fn main() {
    // 构造:字段顺序无所谓,全部字段都要给值
    let mut user = User {
        username: String::from("xiaoling"),
        email: String::from("xl@example.com"),
        age: 26,
        active: true,
    };

    // 访问/修改(修改要求实例是 mut)
    println!("{}", user.username);
    user.age = 27;

    // 取字段的引用(避免 move 走字段),后面调用只读函数常用
    let name_ref: &String = &user.username;
    println!("{name_ref}");
}

💡 注意 username: String拥有型 字段:当整个 user 被 move 或 Drop 时,内部的 String 一并被释放------所有权在这里一层层组合,编译器全自动管好。

6.2.2 字段初始化简写与 .. 更新语法

rust 复制代码
struct User {
    username: String,
    email: String,
    age: u8,
    active: bool,
}

fn build_user(username: String, email: String) -> User {
    // 字段名与变量名相同 → 简写(去掉 username: username 的重复)
    User {
        username,
        email,
        age: 0,
        active: true,
    }
}

fn main() {
    let u1 = build_user(String::from("a"), String::from("a@x.com"));

    // ..u1:其余字段从 u1 "复制/移动"过来(u1.username 等被 move → u1 不可整体再用)
    let u2 = User {
        username: String::from("b"),
        ..u1                 // 等价于手动填 email: u1.email, age: u1.age, active: u1.active
    };
    println!("{} {}", u2.email, u2.age);
    // println!("{}", u1.active); // 可以单独用 u1 没被移动的 Copy 字段
    // println!("{}", u1.username); // ❌ 已被 move 进 u2
}

⚠️ ..u1 会把没显式写出的非 Copy 字段 move 走 ,所以 u1 整体就不能再用了(部分字段还可以)。若想保留 u1,先 u1.clone() 再更新。

6.2.3 三种结构体形态

rust 复制代码
// ① 普通结构体:有名字的字段
struct Rectangle {
    width: u32,
    height: u32,
}

// ② 元组结构体:字段没名字,靠位置区分(用于"包装一个值+类型区分")
struct Color(u8, u8, u8);
struct Point(i32, i32);

// ③ 单元结构体:没有字段(用于实现 trait / 标记,第 8 课会遇到)
struct Marker;

fn main() {
    let rect = Rectangle { width: 10, height: 20 };
    let black = Color(0, 0, 0);
    let origin = Point(0, 0);
    let _m = Marker;

    println!("{}x{}", rect.width, rect.height);
    println!("RGB=({},{},{})", black.0, black.1, black.2);  // 按序号访问
    println!("origin: ({}, {})", origin.0, origin.1);
}

6.3 方法 impl:给类型挂行为

6.3.1 方法与三种接收者

方法与函数区别只有一点:方法在 impl 块里,且第一个参数是 self(自身实例)。

rust 复制代码
struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    // 关联函数:不接收 self(类似静态方法 / User::new)。用 :: 调用
    fn new(width: u32, height: u32) -> Rectangle {
        Rectangle { width, height }
    }

    // 方法一:&self ------ 只读借用(最常见)
    fn area(&self) -> u32 {
        self.width * self.height
    }

    // 方法二:&mut self ------ 修改自己
    fn double_width(&mut self) {
        self.width *= 2;
    }

    // 方法三:self ------ 拿走自己(消费 self,之后实例不可用)
    fn into_string(self) -> String {
        format!("{}x{}", self.width, self.height)
    }
}

fn main() {
    let mut r = Rectangle::new(3, 4);   // 关联函数 ::new
    println!("area = {}", r.area());    // 方法调用 r.method()
    r.double_width();
    println!("now {}x{}", r.width, r.height);

    let desc = r.into_string();         // r 被消费
    // println!("{r:?}");               // ❌ r 已被 move 进方法
    println!("{desc}");
}

三种接收者的选择口诀:

接收者 借用性质 场景 之后还能用实例吗
&self 只读借用 查询/计算 ✅ 能用
&mut self 可变借用 修改自身字段 ✅ 能用
self 拿走所有权 消费自身、返回别的(into_xxx ❌ 不能再碰

💡 &selfself: &Self 的语法糖,Self 指"当前这个类型"。别被各种写法吓到,方法里写的 self 就是"当前实例"。

6.3.2 同名方法名 + 字段名(Rust 允许)

rust 复制代码
struct Circle { radius: f64 }

impl Circle {
    fn radius(&self) -> f64 { self.radius }   // 方法叫 radius,字段也叫 radius,合法
}

fn main() {
    let c = Circle { radius: 1.5 };
    println!("{}", c.radius());    // 带括号=方法,不带=字段:c.radius
}

6.3.3 给枚举也挂 impl

方法不只属于 struct,enum 同样可以(下一节定义完 enum 立刻举例)。

6.4 枚举 enum:表达"是这几种之一"

6.4.1 无数据变体(像"状态列表")

rust 复制代码
enum Platform {
    Ios,
    Android,
    Desktop,
}

impl Platform {
    fn label(&self) -> &'static str {
        match self {
            Platform::Ios => "iOS",
            Platform::Android => "Android",
            Platform::Desktop => "桌面",
        }
    }
}

fn main() {
    let p = Platform::Ios;
    println!("{}", p.label());
}

6.4.2 带数据变体(Rust 的杀手锏)

这是其他语言里 enum 少见的能力------每个变体可以携带不同类型的数据

rust 复制代码
enum Message {
    Quit,                                    // 无数据
    Move { x: i32, y: i32 },                 // 带"命名数据"(像内联结构体)
    Write(String),                           // 带一个 String
    Send { to: String, body: String },       // 带多个
}

fn main() {
    let m1 = Message::Quit;
    let m2 = Message::Move { x: 1, y: 2 };
    let m3 = Message::Write(String::from("hi"));

    // 用 match 拆开处理(穷尽性检查会逼你覆盖每个变体)
    match m2 {
        Message::Quit => println!("退出"),
        Message::Move { x, y } => println!("移动到 ({x}, {y})"),
        Message::Write(text) => println!("写入:{text}"),
        Message::Send { to, body } => println!("发给 {to}:{body}"),
    }
}

用途直击:一个"命令"可以是"登录 / 发送消息 / 切换会话 / 清空历史"里的某一种,每种携带不同参数------第 17 课实战的 UI→Rust 命令通道,就会用带数据 enum 建模命令。

6.4.3 Option<T>:可能"没有值"的类型

Rust 没有 null,用枚举表达"可能为空":

rust 复制代码
enum Option<T> {
    None,          // 没有值
    Some(T),       // 有值,类型 T
}
rust 复制代码
fn main() {
    let some_num: Option<i32> = Some(5);
    let no_num: Option<i32> = None;

    // Option 不能直接当 i32 用!这正是"消灭空指针"的关键:
    // let sum = some_num + 1;          // ❌ error[E0369]: cannot add `i32` to `Option<i32>`
    //  编译器逼你先处理"可能没有值"的情况

    // 解包方式一:match 穷尽处理
    let v = match some_num {
        Some(n) => n,
        None => 0,          // 不写这个分支 → 编译报错(穷尽性)
    };
    println!("{v}");
}

处理 Option 的常用 API(全在标准库,先混脸熟):

rust 复制代码
fn main() {
    let o: Option<i32> = Some(10);

    println!("{:?}", o.is_some());                 // true
    println!("{:?}", o.unwrap_or(0));              // 有值取 10,None 时给默认 0
    println!("{:?}", o.map(|n| n * 2));            // 把 Some(10) 变 Some(20)(闭包见 9 课)
    println!("{:?}", o.ok_or("没有值"));            // 转成 Result(见 6.4.4)

    // unwrap / expect:坚信有值才用,否则 panic
    let n = o.expect("o 竟然为 None?");
    println!("{n}");
}

⚠️ unwrap/expect 在 None 时会 panic。临时练习随便用;真实代码里要在能证明"这里不可能为 None"时才用 (如刚 insert 完的 HashMap::get 二次查询)。7 课会给出更系统的处理策略。

6.4.4 Result<T, E>:可能成功或失败

rust 复制代码
enum Result<T, E> {
    Ok(T),       // 成功,携带值 T
    Err(E),      // 失败,携带错误 E
}
rust 复制代码
fn main() {
    let parsed: Result<i32, _> = "42".parse();      // 字符串→数字:成功是 Ok
    let failed: Result<i32, _> = "abc".parse();     // 失败是 Err

    let r = match parsed {
        Ok(n) => format!("解析成功:{n}"),
        Err(e) => format!("解析失败:{e}"),
    };
    println!("{r}");

    // Err 分支不写?穷尽性检查报错(Result 必须两个分支都覆盖或用通配)
    let r2 = match failed {
        Ok(n) => n,
        Err(_) => -1,          // 通配 _ 忽略错误内容
    };
    println!("{r2}");
}

💡 Option(有无)与 Result(成败)是 Rust 两个"官方钦定"的核心枚举。它们如此常用以至于有专门语法糖(? 运算符、if let.ok_or()),第 7 课错误处理整课都是围绕 Result 展开。

6.5 match 深入:穷尽性、通配、绑定与守卫

6.5.1 穷尽性(exhaustiveness)

match 必须覆盖枚举所有 变体,否则编译不过。这是它作为"安全开关"的根基------以后给枚举加新变体(比如给命令加一种"清空历史"),所有 match 它的地方全都会编译报错,逼你逐个补齐处理。这在实战里是极好的"改一处提醒全流程"机制。

6.5.2 通配 _ 与绑定 @、守卫 if

rust 复制代码
fn main() {
    let x = Some(42);

    // 通配:不关心的值用 _ 或变量名兜底
    match x {
        Some(0) => println!("恰好是 0"),
        Some(_) => println!("是某个非零值"),   // _ 忽略内部值
        None => println!("没有值"),
    }

    // 匹配同时绑定部分内容:@
    match x {
        Some(n @ 1..=10) => println!("1 到 10 之间:{n}"),
        Some(n @ 40..=50) => println!("40 到 50 之间:{n}"),
        Some(_) => println!("其他数"),
        None => println!("None"),
    }

    // 守卫 if:分支条件更复杂时用(守卫不参与穷尽性判断)
    let score = 66;
    match score {
        s if s >= 90 => println!("优秀"),
        s if s >= 60 => println!("及格"),
        _ => println!("加油"),
    }
}

6.5.3 整数匹配的"穷尽"其实是范围

对数字/布尔 match,穷尽性要求覆盖所有可能值,所以一般要么用 1..=10 区间、要么用 _ 兜底。

6.6 if let / while let:只关心一种情况时的简写

当 match 只关心一个变体、其他全忽略时,可以简写:

rust 复制代码
fn main() {
    let config: Option<&str> = Some("release");

    // match 写法
    match config {
        Some(v) => println!("模式:{v}"),
        None => (),
    }

    // if let 简写(更常用、更贴近意图)
    if let Some(v) = config {
        println!("模式:{v}");
    } else {
        println!("未配置");
    }

    // while let:不断尝试直到不是这个变体
    let mut stack = vec![1, 2, 3];
    while let Some(top) = stack.pop() {
        print!("{top} ");      // 3 2 1
    }
    println!();
}

💡 判断标准:只关心一种情况 + 其余忽略 → if let;需要覆盖多种并各自处理 → match 两者不是竞争关系,是详略不同的同一件事。

6.7 为 struct 打印调试:#[derive(Debug)]

前几课我们用 {:?} 打印过数组,但自定义 struct 默认不支持 {:?}

rust 复制代码
#[derive(Debug)]      // 派生宏:自动实现 Debug,让 {:?} 能打印
struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let p = Point { x: 1, y: 2 };
    println!("{p:?}");    // Point { x: 1, y: 2 }
    println!("{p:#?}");   // 多行美化版
}

💡 #[derive(...)] 可以一次列多个:#[derive(Debug, Clone, PartialEq)]。8 课会给一张派生宏速查表;实战建模时每定义一个结构体都顺手 #[derive(Debug, Clone, PartialEq)],这是 Rust 工程里的日常习惯。

6.8 读报错专项:Option 相关的常见错误

① 把 Option 直接当值算

复制代码
let x: Option<i32> = Some(5);
x + 1;    // ❌ error[E0369]: cannot add `{integer}` to `Option<i32>`

修法:先解包(match / if let / unwrap_or / ?)。

② match 忘记 None 分支

复制代码
error[E0004]: non-exhaustive patterns: `None` not covered

修法:补 None => ... 或用 if letunwrap_or

③ unwrap 在 None 上 panic

复制代码
thread 'main' panicked at ...: called `Option::unwrap()` on a `None` value

这是运行期错误(不是编译错误)------练习阶段正常,真实代码请先想清楚会不会 None。

6.9 📝 动手练习

参考实现放 code/06-struct-enum/(写作时同步给出)。本章主题是建模,练习以"用户/订单/会话"为素材,为实战篇热身:

  1. 用户建模 :定义 struct User { id: u32, name: String, email: String, vip: bool },写关联函数 newfn display(&self) -> String;构造 3 个用户放入 Vec<User> 并循环打印(只读遍历用 for u in &users)。
  2. 订单状态机 :定义 enum OrderStatus { Pending, Paid(u32 /*金额*/), Shipped { carrier: String, tracking_no: String }, Cancelled(String) },写 fn describe(&self) -> String 用 match 描述;测试每种状态。
  3. Rectangle 方法集 :给 6.2.3 的 Rectangle 补方法 can_hold(&self, other: &Rectangle) -> boolscale(&mut self, k: u32)to_string_own(self) -> String,并在 main 中分别用 &、&mut、self 接收者调用。
  4. Option 转换练习 :写 fn safe_div(a: i32, b: i32) -> Option<i32>(除零返回 None);再写 fn find_user<'a>(users: &'a [User], id: u32) -> Option<&'a User>(遍历找 id,没有返回 None)------体会返回引用时生命周期如何被自动推断。
  5. if let 训练 :给定 let config = Some(("host", 8080));,用 if let 打印主机与端口,用 while letvec![1,2,3] pop 干净。
  6. 消息系统小建模(综合) :用带数据 enum 实现 Message(如 6.4.2),写 fn handle(msg: Message) -> String,用 match + 守卫对"内容过长"(>20 字符)做专门处理,其余正常返回。再写测试数据跑通。

验收门禁 :能默写 struct 三种形态与 impl 三种接收者;能解释 enum 带数据变体的价值;能说清 Option/Result 的定义(Some(T)/NoneOk(T)/Err(E))与穷尽性检查为什么是 Rust 的"安全开关"。

✅ 本节小结

  • struct :普通/元组/单元三种形态;字段简写与 .. 更新语法;字段默认不实现 Debug,用 #[derive(Debug)]
  • impl :关联函数 Type::new()(无 self)、方法三兄弟 &self/&mut self/self,按"只读/修改/消费"选接收者;
  • enum :可携带不同类型数据的变体,是建模"互斥状态"的最强工具;Option<T>(有无)与 Result<T,E>(成败)是两个核心内置枚举;
  • match :穷尽性逼你覆盖所有分支(新增变体=编译期提醒全链路);_ 通配、@ 绑定、if 守卫;if let/while let 用于只关心单变体;
  • 经验法则 :领域类型用拥有的 String/Vec 而非引用;能 #[derive] 就 derive,别手写样板。

下一课预告 :第 7 课《错误处理:panic、Result 与 ?》------Rust 没有异常(没有 try/catch),它如何优雅处理"会失败的事"?panic! 的两类错误、? 运算符如何把错误一层层往上抛、以及用 thiserror/anyhow 管理真实项目的错误类型。这是做网络/文件/JSON 解析(13-15 课)前必备的一块拼图。

相关推荐
懒羊羊吃辣条1 小时前
Apache IoTDB 实战测试文档+TimechoDB+Workbench+TS file Viewer+Grafana
人工智能·深度学习·时间序列
正经教主1 小时前
【大纲】FDE 前沿部署工程师学习系列教程
人工智能·fde
xsd202411181 小时前
熔炼炉烟尘AI视觉识别:炉前浓烟/淡烟/无烟三分类实时监测
人工智能
foolishlee1 小时前
Rust 下载依赖包
rust
小蒜学长1 小时前
基于Python的微博热搜话题可视化分析系统的设计与实现(代码+数据库+LW)
后端·python·django·数据可视化·情感分析·微博热搜
霸道流氓气质1 小时前
Prompt Caching 完全指南:从入门到精通,掌握 LLM 缓存成本优化
ai
BingoGo1 小时前
Whim 是什么?一门把 PHP 被否决的设想真跑起来的实验语言
后端·php
irislinAmelia1 小时前
Day11文章_多路视频流加权公平调度与令牌桶QoS
c语言·人工智能
梦帮科技1 小时前
从提示词到可复现音乐工作流:RNOISE 2.0 的 Prompt Engineering 架构
人工智能·python·mysql·ci/cd·架构·node.js·numpy