欢迎光临
我们一直在努力

用 AI 学编程的最大误区:不是让 AI 代写代码,而是让 AI 教你理解代码

用 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 的零成本抽象到底层优化,八篇纯技术干货。

    赞(0)
    未经允许不得转载:171主机测评 » 用 AI 学编程的最大误区:不是让 AI 代写代码,而是让 AI 教你理解代码
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址