AI仅模仿代码,编程核心是深度思考与解决复杂问题,这恰恰是AI的短板,无法替代真正程序员的价值。

我们写下的代码,其实远不能完整地展现一个程序实际运行起来的样子。就拿下面这个简单的事件监听器来说吧:

const thing = document.querySelector("#thing");

thing.addEventListener("click", () => {
console.log("this is: ", this);
});

要真正理解这段小脚本,我们需要思考的东西,几乎没有多少是直接体现在这几行文字里的:

这段代码应该如何运行?我们一点头绪都没有。虽然基本的编程直觉告诉我们,需要有个解释器来执行它,但在浏览器这个环境中,整个过程远比想象的要曲折得多。

像 getElementById、addEventListener 和 console.log 这些函数,它们的具体定义并不包含在这段脚本之内。离开了外部环境提供的上下文,我们根本无从知晓它们具体做了些什么。

这段脚本很可能是为特定环境和版本组合设计的,这意味着它可能只在某些特定的环境下才能正常工作。它是否能在所有预期的环境中都顺利运行?这无法通过简单查看代码就得出结论,需要依赖人的经验判断和推理。

那个事件处理器,它只有在发生“点击”事件时才会被触发,这是一种控制反转的模式,意味着代码的执行流并非简单地从上到下顺序进行。

在处理器内部,this 关键字指向的是 window 对象,这一点也极其隐晦。window 同时也是全局对象,可以被隐式引用,这就可能导致我们在不经意间定义了全局变量,从而在其他地方被意外地引用。

此外,这个处理器还默默地忽略了传入的任何参数。

而且,这段代码最终很可能会被打包工具处理。如果我们不额外配置好源码映射,那么一旦运行时出错,看到的堆栈跟踪信息可能就是错乱的,让人难以定位问题。

代码里那个 #thing 引用的是一个 DOM 元素,但它在这里完全是脱离上下文的。这种语法本身又指向了 CSS 选择器——一个完全不同语言体系里的概念。

最后,如果这段脚本在相应的 DOM 元素加载完成之前就执行了,那它也无法正常工作。

这仅仅是四行几乎什么也没做的代码。可想而知,任何一个真实的应用程序,其复杂程度都会远远超过这个例子。

你可能会反驳说:“JavaScript 本身就问题多多,但这不代表所有编程语言都这样吧。”我承认,我选择 JavaScript 正因为它在“代码文本无法充分反映运行时状况”这方面表现得比较极端。但是,你想想看,有限且不可靠的系统资源、并发处理的复杂性、千差万别的运行环境、系统间的集成、内存分配、服务部署、还有那些具体的功能需求等等,你会发现我的观点依然站得住脚:写下来的代码,并不能真实地反映程序运行时的全貌。

当然,我们这些经验丰富的人类工程师,完全有能力理解和推断出程序最终会如何运行。但这个过程恰恰说明,透彻思考所有这些代码本身并未直接体现出来的因素,是多么关键和复杂。

写出看起来似乎能工作的烂代码很容易;而编写出真正健壮、能良好运行的好代码,则要耗费多得多的时间和心力。因此,软件工程的工作流程,更应该是围绕着思考和讨论展开,而不是单纯地敲代码。考虑到现在有海量经过实战检验的软件包和代码示例可供参考,而且大多数软件应用(包、程序、Web 应用等)彼此之间都有很多相似之处,软件工程的真正核心,其实在于深入理解我们正在构建的应用,并为那大约 10% 的、真正与众不同的部分,构思和填补上解决方案。

这也就点明了 AI 编程工具的问题所在。有人可能会兴奋地宣布:“看!我造了个机器人,它能唰唰地产出代码片段,看起来还挺像那么回事!” 但问题是,AI 并不会思考——它只是在预测语言中的模式。换句话说,它写出的,正是那种“看起来似乎能工作”的烂代码。当我们试图使用 AI 生成的代码时,往往会发现:

我们不得不回过头去,仔细检查和验证 AI 到底写了些什么。

对于稍微复杂一点的任务,AI 生成的代码几乎总是错漏百出,因为它根本不具备工程师的判断力和设计能力。

即便我们最终保留了 AI 生成的部分代码,验证它的过程也比验证我们自己写的代码要困难得多。这是因为:

AI 无法真正解释清楚它为什么要这么写。

它常常只是将各种代码片段生硬地拼凑在一起,缺乏内在的逻辑和一致性。

相比于构建清晰、可复用的抽象,AI 更倾向于直接内联各种惯用法,导致代码冗长难读。

结果就是,我们用理解和质量(真正困难且有价值的部分)作为代价,换来了大量劣质代码(容易生成但后续麻烦不断的部分)。总而言之,这是一笔相当糟糕的交易。

相比之下,现有的很多工具和资源要好得多。比如,设计良好的模块提供了清晰的接口、详尽的文档和深思熟虑的抽象,让我们能够轻松地复用代码,不必重复造轮子。项目文档中的示例代码和 Stack Overflow 上的问答,则为代码片段如何工作提供了宝贵的上下文。开源项目更是至少能提供相关的背景信息和项目文档。所有这些,都是比 AI 生成代码更好的选择,它们能帮助工程师专注于真正困难、需要智慧和判断力的工作,并把它们做好。

这让我想起了 Linus Torvalds 在谈到 Linux 内核调试时说过的一段话:

“这不单单是‘源码 vs 二进制’的问题,它涉及的层面更深。关键不在于你必须看源码(当然你得看,任何好的调试器都会让这变得容易)。关键在于,你必须看到源码之上的东西——理解事物的‘意义’。不依赖调试器,你基本上就必须更进一步:去理解程序到底在做什么,而不仅仅是某一行代码。坦白说,对于大多数真正棘手的问题(而不是那些愚蠢的 bug),调试器的帮助并不大。而我真正关心的,正是那些棘手的问题。其余的,都只是细节,早晚会被修复。”

写代码本身或许不难,但要编写出好的程序却异常艰难,这是一场属于思考者的游戏。

本文译自 David Oliver,由 BALI 编辑发布。