适合读者与阅读前提:本文主要面向写过 Java、Python 或其他语言、但从未接触 Rust 的读者。不需要提前了解 Cargo、所有权或 Rust 工程结构;遇到共同概念时,可以借助已有经验理解。
如果你还没有写过代码,也可以跟着读。 建议结合下方 GitHub 仓库里的完整示例,先看代码注释和运行结果,再逐步理解变量、函数、条件和循环。本文围绕读懂 Rust 小程序展开,这些编程基础不会逐项系统讲授;遇到陌生概念时可以先补这一小块,再回来继续,不必一次读完,也不要求一开始就独立写出所有示例。
准备读 Codex 源码,先要认识它的主要实现语言 Rust。我们需要知道程序怎样运行、数据怎样存活、错误怎样表达,以及编译器究竟会替我们检查什么。后面遇到的新语法,很多都与这些选择有关。
例如,函数参数里的 & 与数据的访问方式有关,返回类型里的 Result 与失败的处理方式有关。先知道它们在解决什么问题,再学习写法,源码中的符号就容易联系起来。
全文分为三个部分,可以沿着这条路线读:
- Rust 语言全景:了解程序怎样执行,类型、资源管理、错误和并发各自怎样工作。
- 开发工具与 Cargo:认识 rustc、Cargo 和 rustup,跑通一个小程序。
- 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 本篇回顾
先停在这里,用三个小问题检查理解:
- 为什么
cargo run能直接运行,但 Rust 仍然需要编译?因为 Cargo 把按需构建和运行连在了一起。 - 为什么给教学 Input 增加变体,describe 可能编译失败?因为原来的 match 没有处理新的形态。
Option<String>为 None 和保存空字符串是不是一回事?不是,前者缺失,后者仍有一个字符串值。
能读顺本篇的小程序,就可以先告一段落;不必马上默写所有语法,也不必继续做仓库里的 07 附录。下一篇会从已经见过的 &self 出发,解释谁拥有一个值,以及函数怎样借用它;真实项目中的协议类型、属性和工程结构,等基础更熟悉后再读。
我是程工造Agent,做 Agent 系统与 RAG 的工程师。这个专栏记录我从零学习 Rust、阅读 Codex 源码的过程。
更多文章见知乎专栏「Agent 工程手记」。