Ky不是枕木

分享学习经验

对应代码文件:src/bin/16_async_await.rs

运行命令:

1
cargo run --bin lesson16_async_await

学习目标

异步编程用于在等待 IO、网络或定时器时不阻塞整个线程。Rust 使用 Futureasync.await 表达异步任务。

本节示例保持无外部依赖,重点解释机制。真实项目通常会使用 Tokio 或 async-std 这样的异步运行时。

  • 理解 async fn 返回 Future。
  • 知道 .await 表示等待异步结果。
  • 理解 Future 需要运行时或执行器推动。
  • 区分并发 concurrent 和并行 parallel。

核心概念速查

术语 基本意思 本节用途
async 声明异步块或异步函数。 async fn load() 不会立即执行完所有逻辑,而是返回 Future。
await 等待 Future 完成并取出结果。 只能在 async 上下文中使用。
Future 代表一个未来可能完成的计算。 需要被 poll 推动。
执行器 executor 负责轮询 Future 的运行组件。 Tokio 就提供成熟执行器。
运行时 runtime 提供执行器、定时器、IO 驱动等能力。 网络异步通常离不开运行时。

完整源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use std::task::{Context, Poll, Wake, Waker};

struct NoopWaker;

impl Wake for NoopWaker {
fn wake(self: Arc<Self>) {
// 这个演示用的 Future 会立即完成,所以不需要真正唤醒任务。
}
}

fn block_on_ready<F: Future>(future: F) -> F::Output {
// 真实项目通常使用 Tokio 或 async-std 这样的运行时。
// 这里不用外部依赖,只演示 Future 如何被 poll。
let waker = Waker::from(Arc::new(NoopWaker));
let mut context = Context::from_waker(&waker);
let mut future = Box::pin(future);

match Future::poll(Pin::as_mut(&mut future), &mut context) {
Poll::Ready(value) => value,
Poll::Pending => panic!("这个简单演示只支持立即完成的 Future"),
}
}

async fn fetch_number() -> i32 {
// async fn 返回一个实现 Future 的值;函数体不会立刻执行到完成。
42
}

async fn double_number() -> i32 {
// await 会等待另一个 Future 完成,并取出结果。
let number = fetch_number().await;
number * 2
}

fn main() {
let result = block_on_ready(double_number());
println!("异步计算结果: {result}");

println!("实际网络、文件、定时器异步任务通常需要 Tokio 或 async-std 运行时。");
}

运行与观察

使用 cargo run --bin lesson16_async_await 可以只运行本节示例。

这里的 --bin 后面写的是 Cargo.toml 中声明的目标名,不是 .rs 文件名。文件名用于组织源码,bin 名用于 Cargo 运行。

建议初学时先直接运行,再修改一两行代码观察编译器提示。Rust 的错误信息通常会指出所有权、类型或借用规则哪里不满足。

逐段解读

async fn

异步函数调用后得到 Future,不是马上得到最终值。

.await

在异步上下文中等待另一个 Future 完成。

无依赖示例

本节用标准库展示 Future 被手动推动的基本过程。

真实项目

实际网络、文件和定时器异步通常使用 Tokio 等运行时。

专有词语详解

async

声明异步块或异步函数。

async fn load() 不会立即执行完所有逻辑,而是返回 Future。

await

等待 Future 完成并取出结果。

只能在 async 上下文中使用。

Future

代表一个未来可能完成的计算。

需要被 poll 推动。

执行器 executor

负责轮询 Future 的运行组件。

Tokio 就提供成熟执行器。

运行时 runtime

提供执行器、定时器、IO 驱动等能力。

网络异步通常离不开运行时。

初学者拓展

异步不是自动开新线程。它主要让等待中的任务让出执行权。

并发表示多个任务在时间上交错推进;并行表示多个任务真的在多个 CPU 核心同时运行。

async fn 的返回类型可理解为 impl Future<Output = T>

没有 .await 或执行器推动,Future 可能只是一个尚未运行完成的状态机。

常见误区

  • 不要以为调用 async fn 就会立即执行网络请求。它返回的是 Future。
  • 不要在异步任务里随意执行长时间阻塞操作,否则会卡住运行时线程。
  • 标准库没有内置完整异步运行时,所以真实应用通常需要外部 crate。
  • 异步能提升等待型任务吞吐量,不一定让 CPU 密集计算更快。

进阶练习与参考答案

练习 1:理解 async 返回值

要求:写一个返回数字的 async fn,并说明调用结果是什么。

参考答案:

1
2
3
4
5
6
7
8
async fn answer() -> i32 {
42
}

fn main() {
let future = answer();
drop(future);
}

解释:answer() 返回 Future。没有执行器或 .await,这里不会直接得到 42

练习 2:组合 async 函数

要求:写两个 async 函数,在第三个 async 函数中 await 它们。

参考答案:

1
2
3
4
5
6
async fn left() -> i32 { 20 }
async fn right() -> i32 { 22 }

async fn sum() -> i32 {
left().await + right().await
}

解释:await 只能写在 async 函数或 async 块里。

练习 3:区分并发和并行

要求:用文字说明异步适合什么任务。

参考答案:

1
异步适合网络请求、文件 IO、数据库访问等等待型任务。它让一个线程在等待某个任务时继续推进其他任务。CPU 密集计算通常需要线程池或并行计算库。

解释:这道题没有固定代码答案,重点是理解异步的使用边界。

相关笔记

对应代码文件:src/bin/15_std_functions_macros.rs

运行命令:

1
cargo run --bin lesson15_std_functions_macros

学习目标

本节整理日常 Rust 编程中常见的标准库函数、方法和宏。它们能显著提高代码表达力。

宏看起来像函数,但以 ! 结尾,并在编译期展开。常见宏包括 println!format!vec!dbg!

  • 理解函数、方法和宏的基本区别。
  • 掌握格式化输出、调试输出和集合创建。
  • 会使用 OptionResult 的常用方法。
  • 知道 dbg!assert!panic! 的适用场景。

核心概念速查

术语 基本意思 本节用途
标准库 std Rust 自带的基础功能库。 包括集合、字符串、IO、线程、时间等。
宏 macro 编译期展开的代码生成机制。 ! 结尾,如 println!
format! 生成格式化字符串但不直接打印。 常用于拼接可读文本。
dbg! 打印表达式和位置,返回表达式值。 适合临时调试。
assert! 断言条件必须为真。 常用于测试或检查程序假设。

完整源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
fn main() {
// vec! 创建可增长的 Vec;后续 push 体现它和固定数组的区别。
let mut numbers = vec![1, 2, 3, 4, 5];
numbers.push(6);

// iter/map/filter/collect 是标准库中常用的迭代器组合。
let even_squares: Vec<i32> = numbers
.iter()
.map(|n| n * n)
.filter(|n| n % 2 == 0)
.collect();
println!("偶数平方: {even_squares:?}");

// Option 常用 map、unwrap_or 等方法处理可能不存在的值。
let maybe_name = Some("Rust");
let display_name = maybe_name
.map(str::to_uppercase)
.unwrap_or(String::from("UNKNOWN"));
println!("名称: {display_name}");

// dbg! 会打印表达式和结果,适合临时调试。
let total: i32 = numbers.iter().sum();
dbg!(total);

// format! 生成 String,println! 输出到终端,vec! 创建 Vec。
let message = format!("一共有 {} 个数字", numbers.len());
println!("{message}");

// assert_eq! 常用于测试或运行时检查。
assert_eq!(numbers.first(), Some(&1));
}

运行与观察

使用 cargo run --bin lesson15_std_functions_macros 可以只运行本节示例。

这里的 --bin 后面写的是 Cargo.toml 中声明的目标名,不是 .rs 文件名。文件名用于组织源码,bin 名用于 Cargo 运行。

建议初学时先直接运行,再修改一两行代码观察编译器提示。Rust 的错误信息通常会指出所有权、类型或借用规则哪里不满足。

逐段解读

println!

用于向终端打印文本,支持 {}{:?} 等格式占位。

format!

返回 String,适合构造后续要保存或返回的文本。

vec!

快速创建 Vec,如 vec![1, 2, 3]

Option 方法

unwrap_or 可以在 None 时提供默认值。

专有词语详解

标准库 std

Rust 自带的基础功能库。

包括集合、字符串、IO、线程、时间等。

宏 macro

编译期展开的代码生成机制。

! 结尾,如 println!

format!

生成格式化字符串但不直接打印。

常用于拼接可读文本。

dbg!

打印表达式和位置,返回表达式值。

适合临时调试。

assert!

断言条件必须为真。

常用于测试或检查程序假设。

初学者拓展

println! 是宏,因为它需要支持可变数量参数和格式字符串检查。

dbg! 会取得表达式所有权并返回它。调试非 Copy 值时要注意是否发生移动。

assert_eq!assert!(a == b) 的失败信息更清楚。

标准库方法通常比手写循环更简洁,但初学时应该先理解它们背后的所有权和迭代器行为。

常见误区

  • 不要把 format!println! 混淆。前者返回字符串,后者打印到终端。
  • unwrap_or(default) 会立即计算默认值。默认值计算昂贵时考虑 unwrap_or_else
  • 不要把 dbg! 长期留在正式输出里。
  • 宏不是普通函数,错误信息有时会指向展开后的代码,需要回到宏调用位置理解。

进阶练习与参考答案

练习 1:格式化用户信息

要求:用 format! 生成用户描述字符串。

参考答案:

1
2
3
4
5
6
fn main() {
let name = "Kylin";
let age = 20;
let text = format!("用户 {name}, 年龄 {age}");
println!("{text}");
}

解释:format! 返回 String,后续可以保存或传给函数。

练习 2:Option 默认值

要求:读取可选分数,没有分数时使用 0。

参考答案:

1
2
3
4
5
fn main() {
let score: Option<i32> = None;
let value = score.unwrap_or(0);
println!("{value}");
}

解释:unwrap_or 是处理 None 的常用方法。

练习 3:断言检查

要求:检查数组长度是否为 3。

参考答案:

1
2
3
4
5
fn main() {
let values = vec![1, 2, 3];
assert_eq!(values.len(), 3);
println!("检查通过");
}

解释:assert_eq! 在左右不相等时给出清晰失败信息。

相关笔记

对应代码文件:src/bin/14_lifetimes.rs

运行命令:

1
cargo run --bin lesson14_lifetimes

学习目标

生命周期描述引用有效的范围。它让编译器确认引用不会指向已经释放的数据。

初学时不要把生命周期理解成手动控制内存。它只是对引用关系的标注和检查。

  • 理解生命周期用于防止悬垂引用。
  • 知道很多生命周期可以由编译器自动省略。
  • 会读懂简单的 'a 标注。
  • 理解结构体中保存引用时为什么需要生命周期参数。

核心概念速查

术语 基本意思 本节用途
生命周期 lifetime 引用保持有效的代码范围。 'a 是生命周期参数名。
悬垂引用 dangling reference 指向已经无效数据的引用。 Rust 编译器会阻止它。
生命周期标注 显式说明多个引用之间的有效期关系。 fn longest<'a>(x: &'a str, y: &'a str) -> &'a str
生命周期省略 编译器按规则自动推断常见生命周期。 很多函数不需要手写 'a
‘static 能存活整个程序运行期间的生命周期。 字符串字面量通常是 &'static str

完整源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
fn longest<'a>(left: &'a str, right: &'a str) -> &'a str {
// 生命周期标注说明:返回引用的有效期不超过两个输入引用中较短的那个。
if left.len() >= right.len() {
left
} else {
right
}
}

struct ImportantExcerpt<'a> {
// 结构体保存引用时,需要说明引用必须活得和结构体一样久。
part: &'a str,
}

impl<'a> ImportantExcerpt<'a> {
fn announce_and_return_part(&self, announcement: &str) -> &str {
println!("通知: {announcement}");
self.part
}
}

fn main() {
let first = String::from("short");
let second = String::from("a longer string");
let result = longest(&first, &second);
println!("更长的是: {result}");

let novel = String::from("Rust is safe. Rust is fast.");
let first_sentence = novel.split('.').next().unwrap_or("");
let excerpt = ImportantExcerpt {
part: first_sentence,
};
println!("摘录: {}", excerpt.announce_and_return_part("开始阅读"));
}

运行与观察

使用 cargo run --bin lesson14_lifetimes 可以只运行本节示例。

这里的 --bin 后面写的是 Cargo.toml 中声明的目标名,不是 .rs 文件名。文件名用于组织源码,bin 名用于 Cargo 运行。

建议初学时先直接运行,再修改一两行代码观察编译器提示。Rust 的错误信息通常会指出所有权、类型或借用规则哪里不满足。

逐段解读

返回引用

函数返回引用时,编译器必须知道它来自哪个输入。

longest

longest<'a> 表示返回值不会比两个输入引用中较短者活得更久。

结构体引用字段

结构体保存引用时,要在类型上标注生命周期。

字符串字面量

字面量存放在程序二进制中,常具有 'static 生命周期。

专有词语详解

生命周期 lifetime

引用保持有效的代码范围。

'a 是生命周期参数名。

悬垂引用 dangling reference

指向已经无效数据的引用。

Rust 编译器会阻止它。

生命周期标注

显式说明多个引用之间的有效期关系。

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str

生命周期省略

编译器按规则自动推断常见生命周期。

很多函数不需要手写 'a

‘static

能存活整个程序运行期间的生命周期。

字符串字面量通常是 &'static str

初学者拓展

生命周期标注不会延长任何值的生命。它只是描述已有的有效范围。

'a 的名字没有特殊含义,可以叫 'text,但简单示例常用 'a

返回引用时,返回值必须来自输入引用或其他足够长寿的数据,不能返回局部变量引用。

生命周期和所有权配合工作。拥有值的数据离开作用域后,指向它的引用不能继续存在。

常见误区

  • 不要试图用生命周期标注修复真正的所有权错误。
  • 不要返回函数内部创建的局部 String 的引用。
  • 'static 不等于“永远不释放所有数据”,它表示引用目标在整个程序期间有效。
  • 生命周期报错通常说明引用关系不清楚,先画出数据拥有者和借用者。

进阶练习与参考答案

练习 1:实现 longest

要求:返回两个字符串切片中更长的一个。

参考答案:

1
2
3
4
5
6
7
fn longest<'a>(left: &'a str, right: &'a str) -> &'a str {
if left.len() >= right.len() { left } else { right }
}

fn main() {
println!("{}", longest("Rust", "language"));
}

解释:返回值可能来自任一输入,所以要用同一个生命周期参数描述关系。

练习 2:结构体保存引用

要求:定义保存标题引用的结构体。

参考答案:

1
2
3
4
5
6
7
8
9
struct Title<'a> {
text: &'a str,
}

fn main() {
let raw = String::from("Rust");
let title = Title { text: &raw };
println!("{}", title.text);
}

解释:结构体不能比它内部引用的字符串活得更久。

练习 3:避免返回局部引用

要求:修复返回局部字符串引用的错误。

参考答案:

1
2
3
4
5
6
7
8
fn make_title() -> String {
String::from("Rust")
}

fn main() {
let title = make_title();
println!("{title}");
}

解释:函数内部创建的数据应返回所有权,而不是返回指向局部变量的引用。

相关笔记

对应代码文件:src/bin/13_traits_trait_bounds.rs

运行命令:

1
cargo run --bin lesson13_traits_trait_bounds

学习目标

Trait 定义类型可以具备的行为。Trait bound 则约束泛型类型必须实现某些行为。

如果说泛型回答“我可以接收哪些类型”,trait bound 就回答“这些类型至少要会做什么”。

  • 理解 trait 是行为接口。
  • 会为自定义类型实现 trait。
  • 会用 impl Trait<T: Trait> 写参数。
  • 知道标准库常见 trait,如 DebugDisplayCloneCopy

核心概念速查

术语 基本意思 本节用途
Trait 一组方法签名或默认实现。 类似“能力契约”,但不是类继承。
实现 impl 为某个类型提供 trait 要求的方法。 impl Summary for Article
Trait Bound 泛型参数必须满足的 trait 约束。 T: Summary 表示 T 必须能摘要。
impl Trait 参数或返回值位置的简写形式。 fn print(item: &impl Summary)
默认实现 trait 中直接提供方法体。 实现者可以使用默认逻辑,也可以覆盖。

完整源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
trait Summary {
fn summarize(&self) -> String;

// trait 可以提供默认实现。
fn source(&self) -> String {
String::from("unknown")
}
}

struct Article {
title: String,
author: String,
}

impl Summary for Article {
fn summarize(&self) -> String {
format!("{} by {}", self.title, self.author)
}

fn source(&self) -> String {
String::from("blog")
}
}

fn print_summary<T: Summary>(item: &T) {
// T: Summary 是 trait bound,表示 T 必须实现 Summary。
println!("摘要: {}", item.summarize());
println!("来源: {}", item.source());
}

fn notify(item: &impl Summary) {
// impl Trait 是更简洁的参数写法。
println!("通知: {}", item.summarize());
}

fn main() {
let article = Article {
title: String::from("Learning Rust"),
author: String::from("Kylin"),
};

print_summary(&article);
notify(&article);
}

运行与观察

使用 cargo run --bin lesson13_traits_trait_bounds 可以只运行本节示例。

这里的 --bin 后面写的是 Cargo.toml 中声明的目标名,不是 .rs 文件名。文件名用于组织源码,bin 名用于 Cargo 运行。

建议初学时先直接运行,再修改一两行代码观察编译器提示。Rust 的错误信息通常会指出所有权、类型或借用规则哪里不满足。

逐段解读

定义 trait

trait Summary 声明 summarize 行为。

实现 trait

不同结构体可以各自实现 Summary

Trait bound

fn notify<T: Summary>(item: &T) 接收任何能摘要的类型。

默认方法

trait 可以提供默认方法,减少重复实现。

专有词语详解

Trait

一组方法签名或默认实现。

类似“能力契约”,但不是类继承。

实现 impl

为某个类型提供 trait 要求的方法。

impl Summary for Article

Trait Bound

泛型参数必须满足的 trait 约束。

T: Summary 表示 T 必须能摘要。

impl Trait

参数或返回值位置的简写形式。

fn print(item: &impl Summary)

默认实现

trait 中直接提供方法体。

实现者可以使用默认逻辑,也可以覆盖。

初学者拓展

Trait 表达共享行为,不表达共享字段。不同类型可以用完全不同的数据实现同一个行为。

impl Trait 简洁,适合简单参数。显式泛型 <T: Trait> 适合多个参数需要表达同一类型或多个约束。

一个类型可以实现多个 trait。一个函数也可以要求 T: Summary + Clone

Rust 的 trait 遵循孤儿规则:通常只能为本地类型实现外部 trait,或为外部类型实现本地 trait。

常见误区

  • 不要把 trait 当作父类。Trait 不保存实例字段。
  • 泛型函数里调用 trait 方法前,必须写 trait bound。
  • Debug 用于开发调试,Display 用于用户友好输出,两者不同。
  • Copy 继承自 Clone 的语义,但只有轻量、按位复制安全的类型才应实现。

进阶练习与参考答案

练习 1:定义 Summary

要求:给文章类型实现摘要能力。

参考答案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
trait Summary {
fn summarize(&self) -> String;
}

struct Article {
title: String,
}

impl Summary for Article {
fn summarize(&self) -> String {
format!("文章: {}", self.title)
}
}

fn main() {
let article = Article { title: String::from("Rust") };
println!("{}", article.summarize());
}

解释:trait 只规定行为,具体格式由实现者决定。

练习 2:使用 trait bound

要求:写函数打印任何可摘要对象。

参考答案:

1
2
3
fn print_summary<T: Summary>(item: &T) {
println!("{}", item.summarize());
}

解释:T: Summary 让函数内部可以安全调用 summarize

练习 3:多个约束

要求:写函数要求参数既能摘要又能克隆。

参考答案:

1
2
3
4
5
6
7
fn duplicate_summary<T>(item: &T) -> String
where
T: Summary + Clone,
{
let copied = item.clone();
copied.summarize()
}

解释:where 子句适合约束较多时提高可读性。

相关笔记

对应代码文件:src/bin/12_generics.rs

运行命令:

1
cargo run --bin lesson12_generics

学习目标

泛型让同一段代码适用于多种类型。它能减少重复,同时保持静态类型检查。

Rust 的泛型在编译期单态化,常见情况下不会带来运行时动态分发开销。

  • 理解泛型参数 T 的含义。
  • 会定义泛型函数和泛型结构体。
  • 知道泛型需要 trait bound 才能使用特定能力。
  • 理解 Option<T>Result<T, E> 也是泛型类型。

核心概念速查

术语 基本意思 本节用途
泛型 generic 把具体类型抽象成类型参数。 fn identity<T>(value: T) -> T 可接收多种类型。
类型参数 T 泛型中的占位类型名。 T 不是固定名字,只是惯例。
单态化 monomorphization 编译器为实际类型生成具体版本。 这是 Rust 泛型高性能的原因之一。
泛型结构体 字段类型带类型参数的结构体。 Point<T> 可表示整数点或浮点点。
trait bound 限制泛型类型必须具备某些能力。 下一节会详细讲。

完整源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
fn largest<T: PartialOrd + Copy>(items: &[T]) -> T {
let mut largest = items[0];
for &item in items {
if item > largest {
largest = item;
}
}
largest
}

#[derive(Debug)]
struct Point<T> {
x: T,
y: T,
}

impl<T> Point<T> {
fn x(&self) -> &T {
&self.x
}
}

fn main() {
let numbers = [3, 8, 2, 10];
println!("最大数字: {}", largest(&numbers));

let chars = ['a', 'z', 'm'];
println!("最大字符: {}", largest(&chars));

let integer_point = Point { x: 3, y: 4 };
let float_point = Point { x: 1.2, y: 3.4 };
println!("整数点: {integer_point:?}, x={}", integer_point.x());
println!("浮点点: {float_point:?}, y={}", float_point.y);
}

运行与观察

使用 cargo run --bin lesson12_generics 可以只运行本节示例。

这里的 --bin 后面写的是 Cargo.toml 中声明的目标名,不是 .rs 文件名。文件名用于组织源码,bin 名用于 Cargo 运行。

建议初学时先直接运行,再修改一两行代码观察编译器提示。Rust 的错误信息通常会指出所有权、类型或借用规则哪里不满足。

逐段解读

泛型函数

fn identity<T>(value: T) -> T 接收什么类型就返回什么类型。

泛型结构体

struct Pair<T> 用同一种 T 保存两个值。

多个泛型参数

Result<T, E> 同时有成功值类型和错误类型。

约束需求

如果要比较、打印或相加泛型值,就必须添加相应 trait bound。

专有词语详解

泛型 generic

把具体类型抽象成类型参数。

fn identity<T>(value: T) -> T 可接收多种类型。

类型参数 T

泛型中的占位类型名。

T 不是固定名字,只是惯例。

单态化 monomorphization

编译器为实际类型生成具体版本。

这是 Rust 泛型高性能的原因之一。

泛型结构体

字段类型带类型参数的结构体。

Point<T> 可表示整数点或浮点点。

trait bound

限制泛型类型必须具备某些能力。

下一节会详细讲。

初学者拓展

泛型不是“任意类型随便用”。没有约束时,你只能移动、返回或保存它,不能随便比较或打印。

TUE 只是类型参数名字。E 常用来表示 error 类型。

泛型能把“算法结构”从“具体数据类型”中分离出来。

当两个字段都写 T 时,它们必须是同一具体类型。需要不同类型时写 T, U

常见误区

  • 不要以为泛型函数内部自动知道 T 能打印。打印需要 T: DebugT: Display
  • 如果结构体字段一个是整数、一个是浮点数,就不要都写成同一个 T
  • 泛型参数过多会降低可读性。初学阶段先保持简单。
  • 泛型解决重复类型逻辑,不适合掩盖完全不同的业务概念。

进阶练习与参考答案

练习 1:identity 函数

要求:写一个返回原值的泛型函数。

参考答案:

1
2
3
4
5
6
7
8
fn identity<T>(value: T) -> T {
value
}

fn main() {
println!("{}", identity(10));
println!("{}", identity("Rust"));
}

解释:T 由调用时传入的值推断出来。

练习 2:泛型结构体

要求:定义 Point<T> 并创建整数点和浮点点。

参考答案:

1
2
3
4
5
6
7
8
9
10
struct Point<T> {
x: T,
y: T,
}

fn main() {
let int_point = Point { x: 1, y: 2 };
let float_point = Point { x: 1.0, y: 2.0 };
println!("{} {}", int_point.x, float_point.y);
}

解释:同一个结构体模板可以生成不同具体类型。

练习 3:两个类型参数

要求:定义 Pair<T, U> 保存不同类型。

参考答案:

1
2
3
4
5
6
7
8
9
struct Pair<T, U> {
left: T,
right: U,
}

fn main() {
let pair = Pair { left: "age", right: 20 };
println!("{} {}", pair.left, pair.right);
}

解释:TU 允许两个字段拥有不同类型。

相关笔记

对应代码文件:src/bin/11_error_handling.rs

运行命令:

1
cargo run --bin lesson11_error_handling

学习目标

Rust 把错误分成可恢复错误和不可恢复错误。可恢复错误通常用 Result 表达。

错误处理是 Rust 代码可读性的重要部分。你需要明确说明失败时程序应该如何反应。

  • 理解 Result<T, E> 的含义。
  • 会用 match 处理成功和失败。
  • 掌握 unwrapexpect 的风险和适用场景。
  • 理解 ? 运算符如何传播错误。

核心概念速查

术语 基本意思 本节用途
Result 表示成功或失败的枚举。 Ok(T) 是成功,Err(E) 是错误。
panic 不可恢复错误,程序通常终止当前线程。 适合违反程序假设的严重问题。
unwrap 取出 OkSome 的值,失败时 panic。 初学和测试可用,生产代码要谨慎。
expect 类似 unwrap,但可以提供 panic 信息。 unwrap 更容易定位原因。
? 运算符 成功时取值,失败时提前返回错误。 用于简化错误传播。

完整源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
use std::fs;
use std::io;

fn parse_number(text: &str) -> Result<i32, std::num::ParseIntError> {
// ? 会在错误时提前返回 Err,在成功时取出 Ok 内的值。
let value = text.trim().parse::<i32>()?;
Ok(value)
}

fn read_config() -> Result<String, io::Error> {
// 这里故意读取一个可能不存在的文件,用于演示错误传播。
fs::read_to_string("config.txt")
}

fn main() {
match parse_number("42") {
Ok(value) => println!("解析成功: {value}"),
Err(error) => println!("解析失败: {error}"),
}

match parse_number("not a number") {
Ok(value) => println!("解析成功: {value}"),
Err(error) => println!("解析失败: {error}"),
}

match read_config() {
Ok(content) => println!("配置内容: {content}"),
Err(error) => println!("读取配置失败,但程序继续运行: {error}"),
}

// panic! 用于不可恢复错误;普通业务错误更推荐 Result。
println!("可恢复错误用 Result,不可恢复错误才考虑 panic!。");
}

运行与观察

使用 cargo run --bin lesson11_error_handling 可以只运行本节示例。

这里的 --bin 后面写的是 Cargo.toml 中声明的目标名,不是 .rs 文件名。文件名用于组织源码,bin 名用于 Cargo 运行。

建议初学时先直接运行,再修改一两行代码观察编译器提示。Rust 的错误信息通常会指出所有权、类型或借用规则哪里不满足。

逐段解读

返回 Result

示例函数用 Result<i32, String> 表示可能成功也可能失败。

match 处理

match result 分别处理 Ok(value)Err(message)

expect

用于明确表达“这里失败就说明输入不符合预期”。

? 传播

在返回 Result 的函数中,? 可以减少嵌套匹配。

专有词语详解

Result

表示成功或失败的枚举。

Ok(T) 是成功,Err(E) 是错误。

panic

不可恢复错误,程序通常终止当前线程。

适合违反程序假设的严重问题。

unwrap

取出 OkSome 的值,失败时 panic。

初学和测试可用,生产代码要谨慎。

expect

类似 unwrap,但可以提供 panic 信息。

unwrap 更容易定位原因。

? 运算符

成功时取值,失败时提前返回错误。

用于简化错误传播。

初学者拓展

Rust 没有异常机制作为主路径。失败是类型的一部分,调用方必须面对。

Result<T, E>T 是成功值类型,E 是错误值类型。

? 只能用于返回兼容错误类型的函数。它不是忽略错误,而是把错误交给上层。

库代码通常返回 Result,应用入口可以选择打印错误、重试或退出。

常见误区

  • 不要在所有地方使用 unwrap()。它会让程序在错误输入时直接崩溃。
  • expect() 的信息应该说明你假设了什么,而不是写空泛的 error
  • 使用 ? 前确认当前函数返回 Result 或兼容类型。
  • 不要吞掉错误。至少应该记录、返回或明确处理。

进阶练习与参考答案

练习 1:安全除法

要求:写函数处理除以零错误。

参考答案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
fn divide(a: i32, b: i32) -> Result<i32, String> {
if b == 0 {
Err(String::from("不能除以零"))
} else {
Ok(a / b)
}
}

fn main() {
match divide(10, 2) {
Ok(value) => println!("{value}"),
Err(error) => println!("错误: {error}"),
}
}

解释:除数为 0 是可预期错误,用 Result 返回给调用方。

练习 2:用 ? 传播错误

要求:解析两个字符串并相加。

参考答案:

1
2
3
4
5
6
7
8
9
fn parse_sum(a: &str, b: &str) -> Result<i32, std::num::ParseIntError> {
let left: i32 = a.parse()?;
let right: i32 = b.parse()?;
Ok(left + right)
}

fn main() {
println!("{:?}", parse_sum("40", "2"));
}

解释:? 在解析失败时直接返回 Err,成功时取出数字。

练习 3:替换 unwrap

要求:把 unwrap 改成 match 处理。

参考答案:

1
2
3
4
5
6
7
fn main() {
let raw = "abc";
match raw.parse::<i32>() {
Ok(value) => println!("{value}"),
Err(error) => println!("解析失败: {error}"),
}
}

解释:match 让失败路径也被明确写出来。

相关笔记

对应代码文件:src/bin/10_modules_packages.rs

运行命令:

1
cargo run --bin lesson10_modules_packages

学习目标

模块系统用于组织代码、控制可见性和管理命名空间。Cargo 则负责构建、运行和包管理。

本项目把每节课放在 src/bin 下,并在 Cargo.toml 中声明多个 bin target。

  • 理解 package、crate、module 的基本区别。
  • 掌握 modpubuse 的用途。
  • 知道如何通过 Cargo 运行指定二进制示例。
  • 理解路径中的 crateselfsuper 含义。

核心概念速查

术语 基本意思 本节用途
package 一个 Cargo 管理的项目。 通常由一个 Cargo.toml 描述。
crate Rust 编译单元。 可以是库 crate,也可以是二进制 crate。
module crate 内部的代码组织单元。 mod math 定义一个模块。
pub 公开可见性关键字。 没有 pub 的项默认只在当前模块及子模块内可见。
use 把路径引入当前作用域。 让长路径写起来更短。

完整源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 一个文件内也可以定义模块,便于把相关代码分组。
mod math {
pub fn add(a: i32, b: i32) -> i32 {
a + b
}

pub mod stats {
pub fn average(values: &[f64]) -> f64 {
let sum: f64 = values.iter().sum();
sum / values.len() as f64
}
}
}

// use 可以把路径引入当前作用域,减少重复书写。
use math::stats::average;

fn main() {
let sum = math::add(2, 3);
println!("模块函数 add: {sum}");

let data = [80.0, 90.0, 100.0];
println!("平均值: {}", average(&data));

// 在真实项目中,Cargo.toml 描述 package,src/lib.rs 和 src/bin/*.rs 组织 crate。
println!("本文件用内联 mod 演示模块,保持示例自包含。");
}

运行与观察

使用 cargo run --bin lesson10_modules_packages 可以只运行本节示例。

这里的 --bin 后面写的是 Cargo.toml 中声明的目标名,不是 .rs 文件名。文件名用于组织源码,bin 名用于 Cargo 运行。

建议初学时先直接运行,再修改一两行代码观察编译器提示。Rust 的错误信息通常会指出所有权、类型或借用规则哪里不满足。

逐段解读

内部模块

示例用 mod math 在单文件中定义模块,保持本节自包含。

公开函数

pub fn add 允许模块外部调用。

路径调用

math::add(2, 3) 通过模块路径访问函数。

use 简化路径

use crate::math::add; 后可以直接调用 add()

专有词语详解

package

一个 Cargo 管理的项目。

通常由一个 Cargo.toml 描述。

crate

Rust 编译单元。

可以是库 crate,也可以是二进制 crate。

module

crate 内部的代码组织单元。

mod math 定义一个模块。

pub

公开可见性关键字。

没有 pub 的项默认只在当前模块及子模块内可见。

use

把路径引入当前作用域。

让长路径写起来更短。

初学者拓展

一个 package 可以包含多个二进制 crate。本项目每个 src/bin/*.rs 都是一个独立二进制。

mod 是声明模块。模块代码可以写在同一文件,也可以拆到独立文件。

pub 只公开当前项。结构体公开后,字段仍然默认私有,字段也要单独 pub

use 不会改变所有权,也不会复制代码。它只是让路径在当前作用域更方便。

常见误区

  • 不要把 package、crate、module 混为一谈。它们处在不同组织层级。
  • 函数写了 pub,所在模块如果不可见,外部仍然访问不到。
  • use 引入同名项时可能冲突,需要使用别名 as
  • 多个 src/bin 文件彼此独立,不能直接共享私有函数。共享代码通常放到库模块。

进阶练习与参考答案

练习 1:定义工具模块

要求:定义 utils 模块,公开 double 函数。

参考答案:

1
2
3
4
5
6
7
8
9
mod utils {
pub fn double(value: i32) -> i32 {
value * 2
}
}

fn main() {
println!("{}", utils::double(21));
}

解释:pub 让模块外的 main 能调用 double

练习 2:使用 use

要求:用 use 简化上题的调用路径。

参考答案:

1
2
3
4
5
6
7
8
9
10
11
mod utils {
pub fn double(value: i32) -> i32 {
value * 2
}
}

use utils::double;

fn main() {
println!("{}", double(21));
}

解释:use utils::double 后,当前作用域可以直接写函数名。

练习 3:Cargo 运行指定目标

要求:写出运行第 10 节的命令。

参考答案:

1
cargo run --bin lesson10_modules_packages

解释:--bin 后面跟的是 Cargo.toml 中声明的 bin 名,不是文件名。

相关笔记

0%