用 AI 学编程的最大误区:不是让 AI 代写代码,而是让 AI 教你理解代码
一、一个让我后背发凉的 GitHub 对话
上个月,我在一个 Rust 学习群里看到一段对话。一个刚学 Rust 两周的新人贴了一段代码,问为什么cargo check报错cannot borrow as mutable。
群里有人回:"把这一行改成let mut x = x.clone();就行了。"
新人改了,编译通过,发了个谢谢的表情。
五分钟后,他又贴了一段几乎一模一样的代码,同样的问题。
这不是孤例。群里每天都在发生同样的事:有人给了能编译的改法,没人解释背后的原因。
而 ChatGPT 加剧了这个问题。因为它永远能给出"能跑"的代码。一个初学者用 5 秒钟复制粘贴的解决方案,可能掩盖了一个需要花 2 小时才能真正理解的底层概念。
这就是我这篇文章想说的核心矛盾:你用 AI 学编程的姿势,决定了你是在学编程还是在对 AI 产生不可逆的依赖。
二、错误的 AI 学习路径
左边是大多数人的做法,右边是少数人的做法。两条路径的最终结果天差地别。
三、AI 用法的层次与五步学习法
层次 1:AI 当搜索引擎 —— 最浪费
用户:Rust 怎么读文件? AI: 用 std::fs::read_to_string……你获得了一段代码,但没有获得理解。下次你遇到"读大文件"或者"读二进制文件",你还是不会。
层次 2:AI 当代码生成器 —— 最危险
用户:帮我写一个和多线程安全的缓存 AI: [生成含 Arc<Mutex<HashMap>> 的代码]你获得了一个"能跑"的解决方案,但你不知道为什么用Arc而不仅仅是Rc,不知道为什么Mutex和RwLock的选择取决于读写比例。下一次你换一个场景——比如需要跨tokio::spawn共享这个缓存——你就卡住了。
层次 3:AI 当导师 —— 正确用法
用户:我在学 Rust 的所有权。我写了一段代码,想用一个 Vec 存储一些 String,然后在两个地方同时使用它。编译器报错了。 [粘贴代码和错误信息] *不要给我现成的解决方案代码。* 请帮我: 1. 解释为什么编译器不允许这样做 2. 描述三种不同的解决方法,以及每种方法的 trade-off 3. 画出内存布局图,让我直观理解 AI: 好,我来逐层分析: 1. 为什么编译不过? 因为你把 Vec 的所有权 move 进了第一个使用者。 在 Rust 里,一个值只能有一个所有者。你已经把所有权 交出去了,第二个人就借不到了。 2. 三种方案及 trade-off: 方案 A:clone() - 把整个 Vec 复制一份给第二个人 - 优点:简单,两个副本各自独立 - 缺点:内存翻倍,大 Vec 时不可接受 方案 B:引用 &Vec - 两个人都不获取所有权,只借用 - 优点:零内存开销 - 缺点:必须确保借用期间原数据不被释放 方案 C:Arc (共享所有权) - 包装在 Arc 里,多个人共享同一个数据 - 优点:没有借用检查的麻烦 - 缺点:引用计数开销,不可变的共享 3. 内存布局图: [用 ASCII 或 Mermaid 画出每种方案的内存布局]看到区别了吗?你不再是被动接收代码,而是在和 AI 进行概念层面的对话。
AI 辅助学习的五步法
这是我自己梳理出的学习流程。每学一个新概念,我都按这个顺序来:
第一步:自己先写,写出问题
千万别在白纸上让 AI 写代码。一定要自己先尝试。
/// 你在学 async Rust。自己先写一个"你觉得应该可以跑"的版本: #[tokio::main] async fn main() { let mut data = vec![1, 2, 3]; // 你感觉这样应该可以并发处理 let handle1 = tokio::spawn(async { data.push(4); // 修改 data }); let handle2 = tokio::spawn(async { println!("{}", data.len()); // 读取 data }); // 编译器:error[E0373]: closure may outlive the current function, // but it borrows `data`, which is owned by the current function }现在你有了一个具体的问题,而不是一个泛泛的"教我用 async"。
第二步:问"为什么",不问"怎么做"
用户:上面的代码为什么编译不过?我需要理解编译器说的 'borrows data, which is owned by the current function' 到底是什么意思。请用内存模型的角度解释。这一步的关键是:你的 prompt 里不能出现"帮我写"三个字。
第三步:限制 AI 作答 —— 不给代码
用户:请用以下格式回答: 1. 为什么 data 不能被两个 task 共享(所有权视角) 2. 如果我想让两个 task 都能访问 data,有几种思路? 3. 每种思路的内存布局有什么不同?(用文本图表示) 4. 每种思路的性能影响是什么? *不要给我代码解决方案。*这一步在逼迫 AI 当"老师"而不是"自动补全"。同时你在培养一种习惯:先理解原理,再看实现。
第四步:自己写,让 AI 审
/// 你自己根据理解写出的代码: use std::sync::Arc; use tokio::sync::Mutex; #[tokio::main] async fn main() { let data = Arc::new(Mutex::new(vec![1, 2, 3])); let data_clone = data.clone(); let handle1 = tokio::spawn(async move { let mut guard = data_clone.lock().await; guard.push(4); }); let data_clone2 = data.clone(); let handle2 = tokio::spawn(async move { let guard = data_clone2.lock().await; println!("长度: {}", guard.len()); }); let _ = tokio::join!(handle1, handle2); } // 然后问 AI: // "这段代码有什么我没有考虑到的隐患吗?提示:锁的粒度。" // // AI 可能会指出: // handle2 在持有锁的时候执行 println! —— 如果打印操作很慢, // handle1 会一直被阻塞。应该先 drop guard 再打印。第五步:输出 —— 教会别人,教会自己
我在 CSDN 写博客的最初原因不是为了流量,而是为了检验自己是否真的理解了。如果你不能把一件事解释给一个外行听,你就没有真正理解它。
/// 你学到的是概念,不只是语法: /// /// 1. tokio::spawn 要求闭包是 'static → 需要 move /// 2. 多 task 共享可变数据 → Arc<Mutex<T>> /// 3. 锁的粒度问题 → 尽量缩短持有锁的时间 /// 4. tokio::sync::Mutex vs std::sync::Mutex → /// 异步上下文用 tokio 版本,避免跨 .await 持有标准锁四、核心竞争力与自学建议
现在人人都会用 AI 写代码。那什么才是区分水平的关键?
AI 能生成代码,但不能替代你做这三件事:
辨别力:AI 说在 HashMap 外包 Mutex,但你知道 Reads 远多于 Writes,应该用 RwLock。这个判断来自你对"读写比例"这个场景参数的理解,不是来自代码。
理解力:AI 生成的代码,你能逐行解释它为什么这样写吗?如果不能,它就不是你的代码——它是 AI 的代码,暂时寄存在你的仓库里。
设计力:AI 能帮你实现一个功能,但不能帮你决定"这个功能该不该做"。你的项目的整体架构、技术选型、开发优先级——这些是 AI 的训练数据里没有的上下文。
我给自学者的五条建议
建议 1:每天有一段"无 AI 时间"
每天给自己 30 分钟,关掉 AI,只靠文档和编译器写代码。这 30 分钟在培养的是你的独立思考肌肉。
建议 2:用 AI 生成解释,但自己写代码
我现在的 prompt 模板:
我在学 [概念]。请用三个不同的比喻帮我理解它。 每个比喻对应不同层次的理解: - 给刚学编程的人 - 给有其他语言经验的人 - 给已经理解但想深入的人 不要给我代码。建议 3:用 AI 审代码,不是写代码
把你的代码给 AI,问它:
- 有哪些我没有考虑的边界情况?
- 这段代码在高并发的场景下会有什么问题?
- 如果能改一个地方,你会改哪里?为什么?
建议 4:问问题比要代码有效 10 倍
| 要代码的 prompt | 要理解的 prompt |
|---|---|
| "帮我写一个 WebSocket 客户端" | "WebSocket 和 HTTP 长轮询的本质区别是什么?在什么场景下长轮询反而是更好的选择?" |
| "这个 bug 怎么修?" | "这个 bug 的根因是什么?是不是还有类似的地方可能存在同样的问题?" |
建议 5:建立"我独立完成"的里程碑
每个月或每两周,挑一个任务完全不用 AI。从头到尾靠自己查文档、看源码、调试。这个任务不需要大——写一个命令行参数解析器、实现一个简单的 LRU 缓存——但它必须是你独立完成的。
这些独立完成的时刻,才是你真正在成长的时候。
实操案例:用 AI 导师法学 Rust 的生命周期——我的完整过程
去年 8 月,我遇到了学习 Rust 以来最大的坎:生命周期标注。不是简单的fn foo<'a>(x: &'a str)那种——而是"为什么tokio::spawn要求'static"、"为什么 struct 里不能自引用"、"async fn 里的生命周期到底怎么回事"这类绕不明白的问题。
我没有像一开始那样让 ChatGPT 给出"把生命周期改成'static就行了"的回答。我按着五步法来:
第一步(自己先写):我写了一个简单的任务调度器,想在struct Scheduler里存一个对Config的引用,然后在tokio::spawn里使用它。编译器报了 7 行错,我全部保留了下来。
第二步(问为什么):我把代码和完整编译错误贴给 AI,prompt 是:"请从内存模型角度解释:为什么 spawn 需要 'static?如果 Config 在整个程序运行期间都不会被释放,为什么编译器还是不让我这么做?不要给我改好的代码。"
第三步(限制作答):AI 解释了'static不是"活到程序结束"而是"在任意时刻都可以安全访问"——tokio::spawn不知道 Config 什么时候会被 drop,所以要求闭包内所有引用都必须满足'static。同时解释了 Rust 的生命周期是编译期的保守检查,即使你"知道" Config 会一直活着,编译器不"知道"——它只看类型签名。
第四步(自己写,让 AI 审):我理解后用Arc<Config>把所有权共享出去,重写了调度器。然后问 AI:"我的实现有内存泄漏吗?如果 Config.workers 是 0 会怎样?如果 tokio runtime 在 Config 创建之前就 shutdown 了会怎样?"出来 3 个边界 bug。
第五步(输出博客):花了 2 小时把那天的学习过程写成了一篇博客:"tokio::spawn 的 'static 到底是什么意思"——后来成了我 CSDN 阅读量最高的文章之一。
整个过程比"复制粘贴能跑的代码"多花 3 倍时间,但从此以后,'static、Arc、tokio::spawn这三个概念在我的脑子里是连在一起的,而不是孤立的语法点。这就是我理解的"AI 当导师"——它帮你理解为什么,而你负责把理解内化。
五、总结
写这篇文章的时候,我翻了一下 GitHub Copilot 的使用统计:过去三个月,它提供了 43% 的代码建议,我接受了其中 27%。
这 27% 里,有多少是"理解之后的选择",有多少是"图省事的复制粘贴"?
我诚实地说:差不多对半分。
自学出身让我对"理解"有天然的执念。因为我知道,如果我对一个概念没有彻底的理解,下一次面试官或者线上 bug 就会用它来打我脸。而 AI 恰恰是那种"让你感觉懂了但其实没懂"的工具——就像你看了十分钟教学视频觉得学会了游泳,下水之后才发现手脚不协调。
用 AI 学编程没有错。错的是把 AI 当成知识来源,而不是理解催化剂。
AI 应该是一面镜子,让你看到自己的理解有多薄弱。它不该是一堵墙,挡在你和真正的知识之间。
下周预告:Week 6 —— 性能优化专题。从 profiling 到瓶颈分析,从 Rust 的零成本抽象到底层优化,八篇纯技术干货。