从 0 到 1 学 Codex 源码:Rust 基础(一)

适合读者与阅读前提:本文主要面向写过 Java、Python 或其他语言、但从未接触 Rust 的读者。不需要提前了解 Cargo、所有权或 Rust 工程结构;遇到共同概念时,可以借助已有经验理解。

如果你还没有写过代码,也可以跟着读。 建议结合下方 GitHub 仓库里的完整示例,先看代码注释和运行结果,再逐步理解变量、函数、条件和循环。本文围绕读懂 Rust 小程序展开,这些编程基础不会逐项系统讲授;遇到陌生概念时可以先补这一小块,再回来继续,不必一次读完,也不要求一开始就独立写出所有示例。

准备读 Codex 源码,先要认识它的主要实现语言 Rust。我们需要知道程序怎样运行、数据怎样存活、错误怎样表达,以及编译器究竟会替我们检查什么。后面遇到的新语法,很多都与这些选择有关。

例如,函数参数里的 & 与数据的访问方式有关,返回类型里的 Result 与失败的处理方式有关。先知道它们在解决什么问题,再学习写法,源码中的符号就容易联系起来。

全文分为三个部分,可以沿着这条路线读:

  1. Rust 语言全景:了解程序怎样执行,类型、资源管理、错误和并发各自怎样工作。
  2. 开发工具与 Cargo:认识 rustc、Cargo 和 rustup,跑通一个小程序。
  3. Rust 语法基础:从变量与函数开始,逐步读懂结构体、方法、枚举和可选值。

第一次阅读建议按顺序推进。第一部分先知道各项机制解决什么问题,trait、GC、并发、异步等术语先建立印象即可;第二部分跑通小程序;第三部分跟着代码注释理解基础写法。读完本篇,能说明程序怎样编译运行、读懂示例里的数据与分支,就达到了目标。所有权细则、抽象、异步和真实源码阅读会在后续逐步展开。

配套源码:跟着 GitHub 示例一起读

我把文中的完整程序整理到了自己的 GitHub:NewComerC。可以收藏对应仓库,读文章时随时打开源码对照:

使用的平台 配套仓库
macOS codex-rust-learning-macos
Windows codex-rust-learning-windows

两个版本的教学代码一致,按自己的系统选一个即可。源码可以直接在网页上阅读;需要本地运行时,在仓库页面选择 Code → Download ZIP ,下载并解压,再按仓库首页的 README(项目说明) 准备 Rust 环境。阅读和下载这些公开示例不需要先学 Git。

后文会在相应小节标出示例入口。打开示例目录,先看 README 的运行方法,再读源码和预期输出。本篇只用到 01---06,读到哪一节,就看对应示例;07 是真实源码阅读附录,第一次可以直接跳过。

一、Rust 语言全景:先认识核心特性

先把编译执行、类型、内存资源、错误与并发这几件事连起来看。它们会帮助我们理解,后面的 Rust 代码为什么这样写。

1.1 编译与执行:先生成本地程序

对于我们要研究的本地命令行程序,Rust 的常见流程是:先编译成目标平台的原生程序,再运行生成的可执行文件。

这个过程可以分成三个阶段:

阶段 做什么 得到什么
编写源码 在 .rs 文件中定义数据和逻辑 源代码
编译与链接 检查代码,生成目标平台的机器代码,并完成链接 可执行文件
执行程序 由操作系统加载可执行文件,执行程序逻辑 运行中的进程

Rust 的编译器叫 rustc 。编译好的程序,在兼容的运行环境中,通常不需要再安装 rustc 或后面要介绍的开发工具 Cargo 就能执行。Rust 官方入门

普通 Rust 本地程序不要求由字节码虚拟机执行。它使用编译进程序或通过链接引入的库,并依赖操作系统。产物要匹配操作系统、CPU 架构及所需系统动态库,不能把某个平台的可执行文件随意搬到其他平台运行。Rust 链接说明

1.2 类型与抽象:在运行前明确数据和能力

Rust 是静态类型语言 。一个值是什么类型、能参与哪些操作,受到编译期的类型检查。很多局部变量可以省略类型标注,由编译器根据上下文推断;这些类型仍然会在编译时确定。Rust 类型说明

组织程序时,Rust 用结构体 struct 保存相关数据,用枚举 enum 表达数据的不同形态,用 trait 约定一组能力,再通过组合和泛型复用代码。这里的"泛型"就是让一段逻辑适用于满足要求的不同类型。它支持封装和多态,不提供 class 继承体系;后面会先从 struct 和 enum 学起。Rust 的数据与行为组织

这些抽象也会影响生成的程序。例如,泛型通常会在编译时按实际使用的类型生成具体代码;需要在运行中选择某种实现时,Rust 也提供动态分发。读源码时,既要看一段代码提供什么能力,也要留意它是否涉及额外分配、复制或间接调用,不能只凭"Rust 性能好"就假定没有成本。泛型的编译方式、动态分发

1.3 内存与资源:用所有权决定谁负责清理

程序运行时,字符串、集合等数据需要占用内存,也需要在适当的时候释放。Rust 的常规内存管理不依赖统一的垃圾回收器 GC,而是通过值的所有权安排清理责任。

它采用所有权和借用:所有权回答"这个值由谁负责",借用回答"其他代码可以怎样暂时访问它"。编译器检查这些关系,避免数据已经释放却仍被使用等问题。因此,把一个值交给函数,有时也意味着把管理它的责任交出去,不能总按"多了一份对象引用"理解。

例如,一个局部字符串一直由当前作用域持有,正常离开这个作用域时,就会自动执行清理。规则在编译时检查,实际的内存分配和释放在程序运行时发生。Rust 所有权说明

不依赖 GC,也不代表数据都放在栈上。 栈和堆都是程序运行时使用的内存区域;Rust 的 String、Vec 等类型仍会使用堆内存保存可变长度的数据。后面读到这些类型时,要同时区分"谁拥有数据"和"数据存在哪里"。栈与堆

清理机制还可以用于文件、锁等资源:值被销毁时执行相应的清理操作,Rust 用 Drop 表达这类行为。这也是为什么读源码时,"谁持有这个值、它在什么时候被销毁"会直接影响程序行为,而不只是一个内存优化问题。Drop 文档

1.4 缺失与失败:让可能的结果进入类型

一个函数返回字符串,和"可能返回字符串,也可能没有",是不同的约定。Rust 常用 Option 表示有值或缺失,用 Result 表示成功或失败,让调用者从类型上就能看见需要处理哪些情况。

文件不存在、网络请求失败等日常错误,通常通过 Result 返回或向上传递。Rust 也有 panic 机制,用于无法按正常路径继续的情况,但它不是日常业务错误的常规返回方式。读代码时,要沿着返回值看成功和失败分别怎样处理。Rust 错误处理

1.5 并发与安全:共享访问与任务协作

所有权和类型规则也用于并发:数据能否交给另一个线程、能否被多个线程共享,都需要满足相应条件。比如,不能让一个线程写入普通数据的同时,另一个线程在缺少同步的情况下读写同一位置。这类数据竞争会破坏内存安全,Rust 的安全代码对此提供约束;死锁和业务执行顺序错误仍需要程序员处理。并发设计、数据竞争与一般竞态

这里的"安全代码"指遵守 Rust 安全规则、通过安全接口操作的代码。底层库有时需要使用 unsafe 执行编译器无法完全验证的操作,由实现者负责满足安全约定;unsafe 并不会关闭全部编译检查。上层的安全保证,也依赖这些底层实现履行约定。Unsafe Rust

Rust 还支持 async/await。异步任务由执行器推进,Tokio 这样的库提供任务调度、网络 I/O 和计时器等运行时服务。它可以使用单线程或多线程,异步本身不等于新建线程;这个"异步运行时"也不是执行字节码的虚拟机。Tokio runtime 文档

把这些特点连起来看,Rust 会把类型、数据访问和部分并发问题提前交给编译器检查,让运行中的程序按这些约束工作。程序仍要分配内存、访问文件、等待网络,也仍可能出现逻辑错误;一些安全检查,例如不能预先确定的数组下标检查,也需要在运行时完成。运行时边界检查

有了这层认识,再看具体语法就有了方向。接下来先把程序运行起来,再学习变量、函数与数据类型;所有权、Result 的使用和 trait 的规则留到后续篇目逐步展开。

二、开发工具与 Cargo:编译和运行第一个程序

有了语言全景,接下来把"源码怎样变成运行中的程序"走一遍。先直接调用编译器,再用 Cargo 管理项目,这样三个开发工具的职责就有了具体对应。

2.1 rustc:编译和运行最小程序

配套示例 01-rustc-hello :macOS 版 · Windows 版。目录里的 main.rs 就是这个完整程序:

rust 复制代码
fn main() {
    println!("开始阅读 Codex");
}

fn 用来定义函数,main 是这个可执行程序的入口,花括号里是函数体。这个程序从 main 开始执行,函数体里只有一条打印语句。

println! 把内容打印出来并换行。名字后面的 ! 表明这里调用的是宏。宏可以帮助生成代码,规则和普通函数不完全一样;此刻把它当作常见的打印写法就够了。

按照仓库首页准备好环境后,打开 macOS 的终端或 Windows 的 PowerShell,用来输入命令。先将当前目录切换到解压后的仓库根目录 ,也就是包含 articles 文件夹的那一层。可以输入 cd "本地仓库路径",把引号内的文字换成实际路径;cd 用于切换目录。

下面命令的第一行会进入示例 01,后两行分别编译和运行。macOS 下:

bash 复制代码
cd articles/01-rust-basics-compiling-and-reading/01-rustc-hello
rustc main.rs
./main

Windows 的 PowerShell 下:

powershell 复制代码
cd articles\01-rust-basics-compiling-and-reading\01-rustc-hello
rustc main.rs
.\main.exe

第一行进入示例目录,第二行编译,第三行运行。运行后会看到"开始阅读 Codex"。Windows 下的可执行文件带 .exe;两套命令任选与你系统对应的一套。修改源代码以后,需要重新编译,已有的可执行文件不会自己跟着变化。

如果暂时不准备本地环境,也可以从仓库复制这个完整程序到 Rust Playground 在线运行,先观察输出。后面需要在本地使用 Cargo 时,再回到所选仓库的安装指引;正式安装说明见 Rust 官方安装页。

2.2 rustc、Cargo 与 rustup:三个工具的分工

单独调用 rustc 能编译程序。但一个项目还会涉及使用哪些第三方库、依赖什么版本、怎样组织构建、怎样运行测试。每次都手工管理这些信息会很麻烦。

Cargo 是 Rust 的项目构建和依赖管理工具。 它读取项目配置,组织依赖和编译过程,并提供运行、测试等统一入口,实际编译工作仍会调用 rustc。Cargo 官方入门

如果用过 Maven 或 Gradle,可以借助它们"管理构建与依赖"的职责理解 Cargo。即使只是一个小程序,也可以从一开始就用 Cargo。

开发环境里常见的三个名字,各负责一件事:

工具 主要职责
rustc 检查、编译 Rust 代码
cargo 管理项目构建与依赖,提供运行、测试等入口
rustup 安装和切换 Rust 工具链版本及相关组件

rustup 管的是开发工具链;它不负责充当程序运行时的虚拟机。Rustup 官方说明

2.3 Cargo 项目:目录、配置与运行

配套示例 02-cargo-basics :macOS 版 · Windows 版。仓库已经准备好了一个最小 Cargo 项目。

如果刚运行完示例 01,可以在同一个终端切换到旁边的示例 02;.. 表示上一层目录:

bash 复制代码
cd ../02-cargo-basics

PowerShell 对应写法是 cd ..\02-cargo-basics。如果另开了终端,请按示例 README 中"从仓库根目录执行"的命令进入。先看当前示例里的两个文件就够了:

text 复制代码
02-cargo-basics/
├── Cargo.toml
└── src/
    └── main.rs

src/main.rs 放入口代码,Cargo.toml 描述这个项目。后者是 TOML 格式的配置文件,下面的方括号是在划分配置区域,不是 Rust 源码语法。这个示例的配置如下:

toml 复制代码
[package]
name = "rust_reading"
version = "0.1.0"
edition = "2024"

[dependencies]

这里 [package] 配置包的名字、版本等信息,[dependencies] 列出外部依赖。edition 选择一组语言兼容规则,2024 不是说"安装一个版本号为 2024 的编译器"。

仓库里的 src/main.rs 已经写好了前面的打印程序。在当前 02-cargo-basics 目录,也就是包含 Cargo.toml 的这一层执行:

bash 复制代码
cargo run

Cargo 会按需构建,然后运行程序。如果源码和构建条件没有变化,它可以复用已有结果。一条命令完成编译和运行,不代表 Rust 变成了解释执行。

以后想自己从头创建项目,可以用 cargo new 项目名 生成这样的目录和初始文件。本篇继续使用仓库中准备好的项目即可。

2.4 Cargo 命令:检查、构建、运行与测试

后续最常见的命令也不多:

命令 做什么
cargo check 检查代码及依赖,省去生成目标程序最终可执行文件的步骤,通常较快
cargo build 构建程序,默认使用开发配置
cargo run 按需构建并运行
cargo test 构建并运行测试
cargo build --release 使用 release 配置构建,默认启用更高优化

这些命令也在当前示例目录执行。按默认配置,示例 02 的程序会出现在它的 target/debug/rust_reading;release 产物在 target/release/rust_reading。这里的 rust_reading 来自上面 Cargo.toml 中的 name。Windows 文件名通常带 .exe。这里的 release 是构建配置,不会自动把程序上传或发布出去。Cargo profiles

平时可以先用 check 快速看代码是否有问题。不过 check 通过不等于完整构建一定通过,代码生成或链接阶段仍可能出现错误。cargo check 文档

三、Rust 语法基础:从变量到数据建模

现在程序能运行了,可以把注意力放回代码。这一部分先看单个变量和函数,再把相关数据组织成类型,最后处理"数据有不同形态"和"可能没有值"这两类情况。

配套仓库中的 03---06 各自是独立的 Cargo 项目 。按相应 README 进入包含 Cargo.toml 的示例目录,再执行 cargo run。README 若标注"从仓库根目录执行",先回到包含 articles 的那一层。正文中的变量声明、单个函数和省略函数体的签名是讲解片段;标注"编译失败"的代码则用于说明错误。运行时以仓库里的完整程序为起点,短片段不需要逐段粘贴进项目。先运行、看输出,再尝试修改一个值,会更容易把语法和效果联系起来。

3.1 变量:声明、修改与同名绑定

本节和 3.2 共用示例 03-variables-and-functions :macOS 版 · Windows 版。仓库保存的是 3.2 的完整计算程序;本节先用短片段解释读它需要的变量写法。

3.1.1 let 与类型推断:省略标注不等于动态类型

先看两条可以写在 main 函数体里的变量声明:

rust 复制代码
let used: u32 = 1;
let limit = 3;

let 声明变量,类型写在名字后面:名字: 类型。u32 表示 32 位无符号整数。第二行没写类型,编译器会结合数值和使用方式推断它。

Java 的局部变量类型推断可以帮助理解这一点:省略类型标注,不等于这个变量以后可以任意装不同类型的值。

编译器会为每个绑定确定类型,后续对这个绑定的使用必须符合该类型的要求。许多类型不匹配的问题因此会在运行前被发现。

3.1.2 mut:允许修改变量

下面是故意写错的片段,用来观察不可变绑定的限制;如果把它写在 main 函数体里,会编译失败:

rust 复制代码
let used = 1;
used = 2; // 错误:这个绑定不允许重新赋值

如果确实要修改它,应写成:

rust 复制代码
let mut used = 1;
used += 1;

mut 表明这个绑定允许修改。它也给读源码的人一个提示:这个变量后面的值可能变化。

3.1.3 同名绑定与遮蔽:区分重新绑定和修改

还有一种看起来相似的写法:

rust 复制代码
let used = 1;
let used = used + 1;

第二个 let 创建了同名的新绑定,遮蔽前一个名字,并不是给原绑定重新赋值。这叫 shadowing。先区分"重新绑定"和"修改原绑定"即可,不必马上把所有可变性规则学完。变量与可变性

3.2 函数与表达式:参数、返回值和分号

3.2.1 函数签名:参数和返回类型

回到示例 03 的 src/main.rs,这个完整程序计算三次机会还剩几次:

rust 复制代码
fn remaining(used: u32) -> u32 {
    if used < 3 {
        3 - used
    } else {
        0
    }
}

fn main() {
    for used in [0, 1, 3] {
        let left = remaining(used);
        println!("已用 {used} 次,剩余 {left} 次");
    }
}

它依次输出剩余 3、2、0 次。fn remaining 定义函数,参数 used: u32 写出参数类型,-> u32 写出返回类型。[0, 1, 3] 是数组,for 依次取出其中的值;{used} 和 {left} 把变量填入输出文本。

3.2.2 if 与尾表达式:计算并返回一个值

需要适应的地方是:函数里没有 return。

Rust 中,if 可以是一个产生值的表达式 。条件成立时得到 3 - used,否则得到 0;整个 if 又是函数最后的表达式,所以它的结果成为函数返回值。Rust 函数与表达式

3.2.3 分号与单元值:为什么多一个分号会报错

简单的函数也可以写成这样:

rust 复制代码
fn next_step(step: u32) -> u32 {
    step + 1
}

如果给末尾改成 step + 1;,这个函数就无法通过编译。加分号后,这个表达式的结果不再作为块的结果,函数体会得到单元值 (),与声明的 u32 不符。() 表达"这里没有额外的业务数据",不是 null。

Rust 也能显式写 return step + 1;,尤其适合提前返回。读源码时只要记住:结尾没写 return、也没写分号,很可能正是在返回一个值。

3.3 struct 与 impl:组织数据和方法

配套示例 04-struct-impl-request :macOS 版 · Windows 版。先拆开认识各部分,再读目录中完整的 src/main.rs。

3.3.1 struct:定义字段与数据类型

假设我们要表示一条任务请求,包含文本和允许执行的最大步数。这两项数据可以放进同一个 Request 类型。

定义这样的类型时,先用 struct 描述"有哪些数据",再用 impl 定义"能做哪些操作"。先只看数据部分:

rust 复制代码
struct Request {         // 定义请求类型,把相关字段放在一起
    text: String,       // 请求文本;String 拥有自己的文本数据
    max_steps: u32,      // 最大步数;u32 是 32 位无符号整数
}

上面定义的是类型,还没有创建某一条具体请求。如果 main 函数和这个定义在同一个文件中,可以用下面的片段直接创建一个值。这里先演示基本写法,仓库的完整程序会在下一小节用函数封装这一步:

rust 复制代码
let request = Request {                    // 用结构体表达式创建一个 Request 值
    text: String::from("解释这个项目"),     // 为 text 字段提供值
    max_steps: 3,                          // 为 max_steps 字段提供值
};

这里的 Request { ... } 是结构体表达式 :给出字段的值,得到一个 Request。Request 是类型名,request 是变量名,保存这一次创建的值。这个创建过程不需要先定义名为 new 的函数。结构体表达式

3.3.2 impl 与 new:封装创建过程并定义方法

给 Request 增加两个操作:创建请求,以及判断请求是否可以开始。下面就是示例 04 的完整程序,可以在该示例目录运行 cargo run 对照输出。先跟着注释读,每一行只关注它正在做什么:

rust 复制代码
struct Request {
    text: String,                   // 保存文本
    max_steps: u32,                  // 保存最大步数
}

impl Request {                      // 为 Request 定义关联函数和方法
    fn new(text: String) -> Self {   // 无 self 参数的关联函数,返回一个 Request
        Self {                      // Self 就是 Request;这一步实际构造值
            text,                   // text: text 的简写,左边是字段,右边是参数
            max_steps: 3,           // 给这个字段设置初始值 3
        }                           // 末尾不加分号,把这个值返回
    }

    fn is_ready(&self) -> bool {     // 借用当前 Request 读取信息,返回 true 或 false
        !self.text.is_empty() && self.max_steps > 0 // 文本非空,并且最大步数大于 0
    }
}

fn main() {
    let text = String::from("解释这个项目"); // 调用 from,把字面量转换成 String
    let request = Request::new(text);       // 通过类型找到 new,创建一个请求
    let ready = request.is_ready();         // 检查这个已有请求;自动把它借给 self
    println!("可以开始:{ready}");          // 输出:可以开始:true
}

大写的 Self 在 impl Request 中表示类型 Request。因此,-> Self 表示返回一个 Request,Self { ... } 则与前面写过的 Request { ... } 一样,负责构造值。小写的 self 是方法接收的那个值或引用;本例中的 &self 借用调用者提供的请求。

这里的 new 负责创建值,但它没有特殊的语言地位。 它是我们自己定义的普通关联函数,new 只是常见的命名惯例。把定义中的 new 改成 create,并把调用同步改成 Request::create(text),仍然可以完成同样的工作;Rust 不会自动为这个结构体补出一个 new。关联函数与 new 的命名惯例

把创建过程封装进 new 的作用,是让调用者只提供 text,由函数统一设置 max_steps: 3 等初始值。真正构造 Request 的操作仍在函数体里的 Self { ... } 中;new 把这套步骤组织成了一个可调用的入口。

最后再回看布尔表达式:! 是"非",&& 是"并且"。!self.text.is_empty() 表示文本不为空;println! 后面的感叹号则表示宏调用,要根据位置区分。

3.3.3 关联函数与方法:为什么分别写成 Request::new 和 request.is_ready

impl Request 中定义的函数都与 Request 类型关联,称为关联函数 。其中,第一个参数是 self 接收者的关联函数,又称为方法 ;&self 也是接收者的一种写法。关联函数与方法的定义

回到上面的两个函数,看它们各自需要什么:

函数 是否接收已有的 Request 常见调用写法
new(text: String) -> Self 没有 self 接收者;接收文本并返回一个请求 Request::new(text)
is_ready(&self) -> bool 借用一个已有请求,检查它的字段 request.is_ready()

调用 Request::new(text) 时,通过类型名 Request 找到 new,再把 text 交给它。调用者此时不需要先有一个 Request 值;函数返回后,才把结果保存进变量 request。

调用 request.is_ready() 时,点号左边的 request 就是要检查的那一个请求。Rust 按 &self 的要求把它借给方法,所以括号里不用再写一遍 request。

因此,本例两种调用形式的区别来自函数是否声明了 self 接收者 ,与它是不是叫 new 无关。对于这个没有 self 参数的 new,不能写成 request.new(text)。

方法也可以通过类型路径调用,只是这时要明确传入接收者。对照示例 04 的 main,在 request 已经创建之后,调用 is_ready 的这一行可以有以下两种写法:

rust 复制代码
let ready = request.is_ready();          // 方法调用:自动把 &request 作为 self
let ready = Request::is_ready(&request); // 函数调用:手动传入 &request,结果相同

所以,:: 并不是"只有创建值时才用"的符号,它用来沿路径找到名字。String::from("解释这个项目") 也是先定位 String 关联的 from,再传参调用;后面还会在模块路径和枚举变体中见到 ::。

3.3.4 引用与 &self:怎样访问当前值

&self 是 self: &Self 的简写;放在 impl Request 中,就是 self: &Request。方法接收者的简写规则

下面只对比签名,函数体省略,不能直接运行;实际定义时只选一种:

rust 复制代码
fn is_ready(&self) -> bool { /* 函数体省略 */ }          // 常见简写:共享借用当前值
fn is_ready(self: &Request) -> bool { /* 函数体省略 */ } // 展开看:self 的类型是 &Request

引用用来访问一个已经存在的值。Rust 引用属于指针类型,普通引用要求非空、指向有效数据,并遵守借用规则;这些要求由语言的安全规则约束。Rust 引用与指针

在这个例子里,&self 让方法读取已有的 Request,不复制整个请求,也不接管它;它不提供修改 text、max_steps 这些普通字段的权限。这也解释了前一节的两种调用为什么都是借用 request。

同一个 & 出现在不同位置,读法略有不同:签名中的 &Request 是引用类型,表达式中的 &request 是借出一个引用。方法里仍然可以写 self.text 访问字段,Rust 会处理这里需要的解引用。方法调用与自动借用

后面遇到 &mut self,可以先理解为借来修改;遇到按值接收的 self,则要考虑当前值是否被接管。具体规则在所有权篇展开。

3.4 enum 与 match:表达和处理不同形态

配套示例 05-enum-match-input :macOS 版 · Windows 版。完整程序位于 src/main.rs。

3.4.1 enum:定义可以携带数据的变体

读 Codex 时,我们会遇到各种用户输入:有时是一段文本,有时是一张本地图片。它们都属于"用户输入",但需要的字段不同。

这正适合用 enum 表达。下面是教学简化模型,不是 Codex 的完整协议:

rust 复制代码
enum Input {
    Text { text: String },       // 文本输入,携带一个 String 字段
    LocalImage { path: String }, // 本地图片输入,携带路径;教学例子先用 String
}

fn describe(input: Input) -> String { // 接收一个 Input,返回描述字符串
    match input {                   // 判断是哪一种输入,并取出它携带的数据
        Input::Text { text } => format!("文本:{text}"), // 匹配 Text,把字段绑定为 text
        Input::LocalImage { path } => format!("本地图片:{path}"), // 匹配图片,取出 path
        // => 左侧是匹配模式,右侧产生结果;format! 返回字符串,不直接打印
    }
}

fn main() {
    let input = Input::Text {              // 这里是构造:选择 Input 的 Text 形态
        text: String::from("看看这个项目"), // 给这个形态的 text 字段赋值
    };
    println!("{}", describe(input));       // 调用 describe,再打印返回的字符串
}

它也是可运行的完整程序。Input 定义两种形态,Rust 把它们叫作"变体":Text 携带文本,LocalImage 携带路径。一个 Input 值在某一时刻属于其中一种形态。

Rust 的 enum 可以让各个变体携带不同类型、不同数量的数据,因此适合表达"同一类输入有几种不同形态"。Rust 枚举

如果用一个大结构体同时放文本字段和图片字段,还得额外判断"两者都有""两者都没有"算不算合法。enum 则可以把允许的形态直接写进类型定义。

3.4.2 match:匹配变体、取出字段并覆盖所有情况

上面 describe 的核心是 match。以这一支为例:

rust 复制代码
Input::Text { text } => format!("文本:{text}") // 左边匹配并取字段,右边生成描述

它表示:如果 input 是 Text 变体,就把它的 text 字段绑定出来,再生成描述。main 中的 Input::Text { ... } 在创建一个值,match 中相似的写法则在识别并拆开已有值;同一对花括号,要结合所在位置理解。

format! 和 println! 都支持格式化文本,但前者返回一个 String,后者直接打印。两个分支都产生 String,整个 match 就可以作为 describe 的返回结果。

match 还有一个很有用的要求:覆盖可能出现的情况。在这个例子中,如果给 Input 新增 Audio 变体,却没有相应修改 match,编译器会提示匹配不完整。也可以使用 _ 兜底,不过那样新增变体时,就不一定会提醒你补业务处理。

3.5 Option:明确表示有值或缺失

配套示例 06-option-title :macOS 版 · Windows 版。完整程序位于 src/main.rs。

任务标题可能没有填写。在 Rust 中,可以写 Option<String>,表示这里可能有一个 String,也可能没有。

它只有两种形态:Some(值) 和 None。结合前面的 enum 和 match,来看这段完整程序怎样处理它们:

rust 复制代码
fn main() {
    let title: Option<String> = None;       // 标题可能有值;这里明确表示没有
    let display_title = match title {      // 把两种可能都处理成一个 String
        Some(text) => text,                // 有标题,就取出里面的字符串
        None => String::from("未命名任务"), // 没有标题,就创建默认字符串
    };
    println!("{display_title}");           // 输出:未命名任务
}

Option<String> 和 String 是不同类型,不能把前者直接当成后者使用。这里通过匹配,明确决定缺失时给出什么结果;Some(String::from("")) 则表示存在一个空字符串,与 None 不同。Option 文档

如果用过 Java Optional,可以借助它"明确表示值可能缺失"的用途理解 Option。上面的 match 明确处理了 Some 和 None,让"缺少标题时怎么办"直接出现在代码里。

3.6 本篇回顾

先停在这里,用三个小问题检查理解:

  1. 为什么 cargo run 能直接运行,但 Rust 仍然需要编译?因为 Cargo 把按需构建和运行连在了一起。
  2. 为什么给教学 Input 增加变体,describe 可能编译失败?因为原来的 match 没有处理新的形态。
  3. Option<String> 为 None 和保存空字符串是不是一回事?不是,前者缺失,后者仍有一个字符串值。

能读顺本篇的小程序,就可以先告一段落;不必马上默写所有语法,也不必继续做仓库里的 07 附录。下一篇会从已经见过的 &self 出发,解释谁拥有一个值,以及函数怎样借用它;真实项目中的协议类型、属性和工程结构,等基础更熟悉后再读。


我是程工造Agent,做 Agent 系统与 RAG 的工程师。这个专栏记录我从零学习 Rust、阅读 Codex 源码的过程。

更多文章见知乎专栏「Agent 工程手记」。

相关推荐
恋喵大鲤鱼1 小时前
Rust 依赖包管理
rust
znnnk2 小时前
2026 前端工程化新纪元:Rust 重塑工具链,AI Agent 接管流水线
前端·人工智能·rust
小七-七牛开发者16 小时前
周一上线 | DeepSeek 开始卷“许愿式编程”,Agent 手机来了,Character.AI 申请破产
ai·大模型·agent·claude·codex·deepseek·ai coding
小郭哥x1 天前
Codex 桌面版(Windows)启动后无 UI 窗口问题
chatgpt·openai·codex
gopher95111 天前
RUST:序列化与反序列化 serde + derive 详解
开发语言·后端·rust
.Bryce.1 天前
你了解 Blackboard(黑板系统)吗?听说过基于黑板系统实现的智能体吗?
java·网络·gpt·php·agent·codex·黑板系统
遇码1 天前
流式调用中途断连别直接报错:瞬态故障识别、退避重试与中文根因诊断的搭建方法
大数据·人工智能·ai·rust·数据分析·大模型·agent
DongQiShanRen1 天前
六根硬件映射(四):归属澄清、两处不一致与卷十一总表
开发语言·人工智能·算法·rust·哈希算法·集成学习
alwaysrun2 天前
Web之wasm-pack详解
前端·rust·web·wasm