Rust 学习中的费曼技巧:让 AI 不断追问"本质是什么",直到你讲清楚
一、费曼技巧 + AI 的组合为什么有效
费曼学习法的核心就四步:
而 AI 在这里扮演的角色是 "永不疲倦的追问者":
flowchart TB
A[选择要学的概念<br/>如: Rust 生命周期] –> B[用自己的话解释给 AI 听]
B –> C{AI 追问}
C –>|"本质是什么?"| D[反思: 我有没有触及本质?]
C –>|"能举个反例吗?"| E[反思: 我是否知道边界?]
C –>|"和 XXX 有什么区别?"| F[反思: 我是否理解了差异?]
C –>|"能不能用一行代码说清?"| G[反思: 我的理解够精炼吗?]
D –> H{能回答吗?}
E –> H
F –> H
G –> H
H –>|能| I[概念真正掌握了]
H –>|不能| J[找到知识盲区]
J –> K[查文档 / 写代码验证]
K –> B
style C fill:#ffd93d,stroke:#333
style I fill:#6bcb77,stroke:#333
style J fill:#ff6b6b,stroke:#333
传统自学的痛点:看书时觉得懂了,一写代码就懵。原因是被动接收信息造成的"流畅度错觉"——你认出了这个概念,并不代表你理解了它。
而 AI 追问 + 费曼讲解的组合,强制你主动生成解释,把模糊的"感觉懂了"变成清晰的"能讲明白"。
二、实战案例 1:用费曼法理解 Rust 的 &T vs T
第一步:选一个看起来简单但容易混淆的概念
&T(不可变引用)和 T(所有权)的区别,初学者面试高频踩坑题。
第二步:向 AI 解释你的理解
我的第一次解释:"&T 是借用的,不拥有数据;T 是拥有的,可以随便改数据。"
第三步:AI 追问
AI: "那为什么 &T 不能改数据,但 &mut T 能?这是 Rust 的规则,还是内存层面的本质区别?"
我愣住了——我之前只是记住了规则,没有理解本质。
第四步:回头查资料,重新理解
翻 Rust 官方文档和 Rustonomicon 后,我更新了理解:
修正后的解释:"从内存层面来说,T 表示你拥有这块内存——如果它在线性内存中被分配,你在 T 离开作用域时负责释放它。&T 只是一个不拥有内存的指针,它只是一个地址。之所以 &T 不能修改数据,是因为 Rust 的所有权规则不允许通过共享引用修改值——这个限制不是硬件限制,而是编译器插入的安全检查。"
// ========== 用代码验证理解 ==========
fn demonstrate_ownership_vs_reference() {
// T: 拥有数据的所有权
let s1 = String::from("hello");
// s1 是 String 的所有者 ——
// 在栈上有一个 String 结构体(ptr + len + cap)
// 在堆上有 "hello" 的实际字节数据
// 当 s1 离开作用域,堆内存被自动释放
// &T: 不拥有,只借来看
let s2: &String = &s1;
// s2 只是一个指针(8 字节的地址),指向 s1 在栈上的位置
// s2 不负责释放任何内存
// s2 离开作用域时,什么都不发生
// ❌ 这样不行 —— 因为 s1 还活着,你不能同时有可变引用
// let s3: &mut String = &mut s1;
println!("s1={s1}, s2={s2}");
}
第五步:再给 AI 讲一遍,看能不能通过追问
最终解释:"T 和 &T 的本质区别是对内存的控制权。T = 你全权管理这块内存,可以修改它,也必须负责释放它。&T = 你借了一个望远镜看别人管理的内存,只能看不能碰。Rust 编译器在编译期追踪谁有什么权限,确保不会出现一个人在看的时候另一个人在改。"
这次 AI 追问:"那 Box<T> 呢?它也是 T 吗?"——我能回答了:"Box<T> 把 T 从栈上移到堆上,但所有权规则不变。它只是改变了分配位置,不改变所有权语义。"
这就是费曼法的威力:AI 的追问不断逼迫你从不同角度审视同一个概念,直到你能扛住所有方向的提问。
三、实战案例 2:用费曼法理解异步 Rust 的 Future
异步 Rust 是我自学过程中最大的坎之一。下面展示费曼法的完整对话流程:
我的第一次讲解:"Future 就是一个可能还没完成的计算,.await 会等待它完成。"
AI: "那 std::future::Future 和 async fn 是什么关系?"
我: "async fn 会自动生成一个实现了 Future trait 的类型……嗯,具体怎么生成的我也不太清楚。"
AI: "那你能解释 Future 的状态机是什么吗?"
到这里我就卡住了。然后去翻了编译器展开 async fn 后的代码:
// ========== 查资料后理解:async fn 的状态机展开 ==========
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
// 原始写法 —— 使用 async fn
async fn fetch_data(url: &str) -> Result<String, String> {
let response = reqwest::get(url).await
.map_err(|e| format!("请求失败: {e}"))?;
let body = response.text().await
.map_err(|e| format!("读取失败: {e}"))?;
Ok(body)
}
// 编译器将它大致展开为(简化版):
enum FetchDataFuture<'a> {
// 状态 0: 还没开始
Start { url: &'a str },
// 状态 1: 第一次 .await 后,等待 get 完成
AwaitingGet {
get_future: Pin<Box<dyn Future<Output = Result<reqwest::Response, reqwest::Error>> + 'a>>,
},
// 状态 2: get 完成,等待 text 完成
AwaitingText {
response: reqwest::Response,
text_future: Pin<Box<dyn Future<Output = Result<String, reqwest::Error>>>>,
},
// 状态 3: 完成
Done,
}
impl<'a> Future for FetchDataFuture<'a> {
type Output = Result<String, String>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
// 这里用 unsafe 因为 Pin 要求不自移
// 实际编译器生成的代码用大量 unsafe 优化
// 但逻辑等价于下面的 match:
let this = unsafe { self.get_unchecked_mut() };
loop {
match this {
FetchDataFuture::Start { url } => {
let get_future = Box::pin(reqwest::get(url));
*this = FetchDataFuture::AwaitingGet { get_future };
// 继续循环,进入下一个状态
}
FetchDataFuture::AwaitingGet { get_future } => {
match get_future.as_mut().poll(cx) {
Poll::Ready(Ok(response)) => {
let text_future = Box::pin(response.text());
*this = FetchDataFuture::AwaitingText {
response, // 简化:实际有所有权问题
text_future,
};
}
Poll::Ready(Err(e)) => {
return Poll::Ready(Err(format!("请求失败: {e}")));
}
Poll::Pending => return Poll::Pending,
}
}
FetchDataFuture::AwaitingText { text_future, .. } => {
match text_future.as_mut().poll(cx) {
Poll::Ready(Ok(body)) => {
return Poll::Ready(Ok(body));
}
Poll::Ready(Err(e)) => {
return Poll::Ready(Err(format!("读取失败: {e}")));
}
Poll::Pending => return Poll::Pending,
}
}
FetchDataFuture::Done => {
panic!("不应在 Done 状态再次 poll")
}
}
}
}
}
修正后的讲解:"Future 的本质是一个状态机。async fn 里的每个 .await 点,就是状态机的一个状态分界。编译器把你的 async fn 变成:
AI 继续追问: "那 Pin 是干什么的?为什么 Future 需要它?"
我的回答:"Pin 保证了 Future 在内存中不会移动。因为 Future 可能在 poll 时保存了指向自身的指针(比如在局部变量里缓存了另一个 Future 的引用)。如果 Future 被移动,这些指针就悬空了。Pin 在类型层面保证'这个值不会在内存中移动'。"
到了这一步,我对 Future 的理解已经远超"会用 async/await"的水平了。
四、费曼法 + AI 的具体操作流程
基于我的实践,总结出可操作的五步流程:
flowchart LR
S1[1. 声明目标<br/>"我今天要讲清楚 X"] –> S2["2. 裸讲<br/>(不查资料先讲一遍)"]
S2 –> S3["3. AI 追问<br/>(本质/区别/反例)"]
S3 –> S4["4. 查缺补漏<br/>(看源码/写测试)"]
S4 –> S5["5. 复盘验证<br/>(换个方式再讲一遍)"]
S5 –>|通过| DONE[✅ 真正理解]
S5 –>|卡壳| S4
style S3 fill:#ffd93d,stroke:#333
style DONE fill:#6bcb77,stroke:#333
第一步:声明具体目标
不要说"我要学 Rust",要说"我要理解 Mutex<T> 和 RwLock<T> 的内部实现原理和使用场景差异"。
第二步:裸讲——不看资料先讲
打开 AI 对话窗口,用自己的话解释这个概念。这一步最关键——你发现自己讲不清楚的地方,就是你的知识盲区。有时候你以为自己懂了,一张嘴就发现全是模糊的"好像是"。
第三步:让 AI 结构化追问
给 AI 一段 prompt:
"我想用费曼学习法理解 Rust 的 [概念]。我会给你讲解,请你不断追问:
第四步:查缺补漏
AI 问到你答不上来的地方,就是你真正的知识边界。这时候去查:
// ========== 用可运行的测试来验证理解 ==========
#[cfg(test)]
mod learning_tests {
use std::sync::{Arc, Mutex, RwLock};
use std::thread;
#[test]
fn mutex_guarantees_exclusive_access() {
// 验证: Mutex 在同一时刻只允许一个线程访问数据
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
let counter = Arc::clone(&counter);
handles.push(thread::spawn(move || {
let mut num = counter.lock().unwrap();
*num += 1;
// lock 在这里自动释放(离开作用域)
}));
}
for handle in handles {
handle.join().unwrap();
}
assert_eq!(*counter.lock().unwrap(), 10);
// 如果 Mutex 没有正确工作,结果不会是 10
}
#[test]
fn rwlock_allows_concurrent_reads() {
// 验证: RwLock 允许多个读者同时持有读锁
let data = Arc::new(RwLock::new(vec![1, 2, 3]));
let mut handles = vec![];
// 多个线程同时读取
for i in 0..5 {
let data = Arc::clone(&data);
handles.push(thread::spawn(move || {
let guard = data.read().unwrap();
// 所有线程可以同时持有读锁
println!("线程 {i} 读到: {guard:?}");
}));
}
for handle in handles {
handle.join().unwrap();
}
// 如果没有死锁,说明并发读正常
}
#[test]
fn rwlock_writer_blocks_readers() {
// 验证: 写者持有锁时,读者必须等待
let data = Arc::new(RwLock::new(0));
let data_clone = Arc::clone(&data);
let writer = thread::spawn(move || {
let mut guard = data.write().unwrap();
*guard = 42;
// 写锁持有期间…
thread::sleep(std::time::Duration::from_millis(10));
// 写锁在这里释放
});
// 读者必须等待写者完成
let reader = thread::spawn(move || {
let guard = data_clone.read().unwrap();
assert_eq!(*guard, 42); // 一定是写者更新后的值
});
writer.join().unwrap();
reader.join().unwrap();
}
}
写测试来验证理解是我学到的最强技巧。当你对某个概念不确定时,写一个测试——如果测试按你的预期过了,说明你的理解正确;如果过不了,说明你的心智模型有 bug。
第五步:复盘验证——换个方式再讲一遍
用另一种角度重新解释同一个概念。比如,第一遍用"内存模型"的角度讲 &T vs T,第二遍用"权限系统"的角度讲。如果能从两个角度都讲清楚,这个概念就真正掌握了。
五、总结
这篇文章介绍了用费曼学习法 + AI 来深度理解 Rust 核心概念的方法:
费曼法的本质是"教是最好的学":你只有在能用自己的话讲清楚一个概念时,才算真正理解它。AI 作为"永不疲倦的追问者"是这个方法的完美搭档。
裸讲是发现盲区的最高效方式:不查资料,直接对着 AI 讲一个概念。你卡壳的地方就是需要补的课。这个过程可能只需要 10 分钟,但效果超过 1 小时的被动阅读。
AI 结构化追问比"随便问问"有效得多:让 AI 从"本质""反例""区别""浓缩"四个角度追问,形成完整的理解闭环。
写测试验证心智模型:代码不会自欺欺人。如果你"觉得"理解了并发安全,写一个多线程测试看看能不能通过。
作为自学编程的学习者,我最大的体会是:不要追求"看完"一本教程,要追求"讲清楚"一个概念。贪多嚼不烂,把一个概念讲到能扛住 AI 的十轮追问,比泛泛看完十篇文章有用得多。
希望这个方法能帮到同样在学 Rust 的朋友。有问题欢迎评论区交流!





