布莱恩·柯尼汉关于“良好品味”的建议表明:可读的代码能节省时间、减少 bug,并让真实团队比聪明技巧更快地前进。

布莱恩·柯尼汉的名字出现在许多开发者习以为常的地方:经典的 Unix 工具、C 生态,以及几十年来教人如何清楚地解释程序的著述。不管你是否记得《The C Programming Language》(与 Dennis Ritchie 合著)、《The Unix Programming Environment》,或是他的随笔与演讲,贯穿其间的共同点是对简单想法清晰表达的坚持。
克尔尼汉最好的建议并不依赖于 C 的语法或 Unix 的惯例。它关注的是人类如何阅读:我们在扫描结构、依赖命名、推断意图,当代码把含义藏在技巧后面时我们就会困惑。这就是为什么在你写 TypeScript、Python、Go、Java 或 Rust 时,可读性的“品味”仍然重要。
语言会改变,工具会改进。团队仍然在时间压力下交付功能,而且大多数代码由原作者之外的人维护(常常是未来的你)。清晰是让这一切可持续的放大器。
这不是对“英雄式编码”的致敬,也不是要背诵老派规则。它是一份关于那些使日常代码更容易维护的习惯的实用指南:
克尔尼汉的影响重要在于它指向一个简单且对团队友好的目标:写出会交流的代码。当代码读起来像清晰的解释时,你花在解码上的时间更少,改进上的时间更多。
“好品味”并不是关于个人风格、花哨的模式,或把解决方案压缩到最少行数。它是一种习惯:选择最简单且清晰的选项,以可靠地传达意图。
一个有好品味的解决方案能为下一位读者回答一个基本问题:这段代码想做什么,为什么要这样做? 如果答案需要心智体操、隐藏的假设或解码聪明技巧,那这段代码就在消耗团队时间。
大多数代码被阅读的频率远高于被编写。 “好品味”把阅读当作首要活动:
因此,可读性不仅仅是美观(缩进、行宽或你是否喜欢 snake_case)。那些有用,但“好品味”主要是让推理变得容易:清晰的命名、明显的控制流与可预测的结构。
一个常见错误是为了简洁而牺牲清晰。有时最清晰的代码会稍微长一点,因为它把步骤写得更明确。
例如,比较两种方法:
第二种可能增加几行,但降低了验证正确性所需的认知负担,也使得定位 bug 更容易、变更更安全。
好品味就是知道何时停止用聪明技巧“改进”解决方案,而改为把意图写清楚。如果队友能在不向你请教的情况下理解代码,你就做得很好。
聪明代码在当下常常让人觉得是胜利:更少的行、更巧的伎俩、diff 中的“哇”因素。但在真实团队中,那种聪明会变成反复出现的账单——以入职时间、审查时间和每次有人不得不接触代码时的犹豫来支付。
入职变慢。 新成员不仅要学产品,还要学你的私有捷径。如果理解一个函数需要解码聪明的操作或隐式约定,人们会避免修改它——或者带着恐惧去改它。
审查变长且不可靠。 审查者会消耗精力去证明技巧是正确的,而不是评估行为是否符合意图。更糟的是,聪明代码更难以在脑中演算,审查者因此更容易错过本可以在更直白版本中发现的边界情况。
聪明会在以下场景中复利:
一些常见的罪魁祸首:
17、0.618、-1),它们编码了没人记得的规则。克尔尼汉关于“品味”的观点在这里体现:清晰不是写更多代码,而是让意图显而易见。如果一个“聪明”版本今天省下 20 秒,但每个未来读者都要多花 20 分钟,那它并不聪明——它很昂贵。
克尔尼汉的“品味”常常体现在一些小而可复现的决定中。你不需要大刀阔斧的重构就能让代码更易维护——微小的可读性胜利在每次有人扫描文件、搜索某个行为或在时间压力下修复 bug 时都会累积收益。
一个好名字能减少注释的需求并让错误更难藏匿。
目标是意图揭示型的名字,符合团队的说法:
invoiceTotalCents 而不是 sum。如果一个名字让你必须去解码它,它就在做相反的工作。
大多数阅读是扫描。统一的空白和结构帮助眼睛找到重要部分:函数边界、条件和“正常路径”。
几个实用习惯:
当逻辑变得棘手时,通过把决策写得显式通常能提升可读性。
比较两种风格:
第二种更长,但读起来像一个检查清单——且在扩展时更不易出错。
这些是“微小”的选择,但它们构成了可维护代码的日常工艺:诚实的命名、能引导读者的格式和不会让人做心智体操的控制流。
克尔尼汉式的清晰在如何将工作拆分为函数与模块时最为明显。读者应当能快速浏览结构、猜到每个部分的作用,并在读细节前大致正确。
目标是让函数在同一“缩放层级”下只做一件事。当函数混合了验证、业务规则、格式化和 I/O 时,读者需要同时关注多条线索。
一个快速测试:如果你发现自己在函数内写注释像“// now do X”,通常 X 就适合抽成一个具名且清晰的单独函数。
长参数列表是隐藏的复杂性税:每个调用点都变成一个小型配置文件。
如果多个参数总是一起传递,合理地将它们分组。选项对象(或小数据结构)可以使调用点自解释——前提是你保持分组的连贯性并避免把一切都塞进“杂项”包里。
另外,偏好传递领域概念而非原始类型。UserId 优于 string,DateRange 优于 (start, end),当这些值有规则时尤其如此。
模块就是承诺:“关于这个概念你需要的都在这里,其余在别处。” 保持模块足够小以便能在脑中记住它们的目的,并设计出最小化副作用的边界。
有助的实用习惯:
当你确实需要共享状态时,要诚实命名并记录不变式。清晰不是避免复杂,而是把复杂放在读者期望的位置。更多关于在变更过程中保持这些边界的内容,见 /blog/refactoring-as-a-habit。
克尔尼汉的“品味”也体现在注释方式上:目标不是注释每一行,而是减少未来的混淆。最好的注释能防止错误的假设——尤其是在代码正确但令人惊讶的情况下。
复述代码的注释(“i 自增”)只是添乱,会教读者忽视注释。好注释解释意图、权衡或语法无法显现的约束。
# Bad: says what the code already says
retry_count += 1
# Good: explains why the retry is bounded
retry_count += 1 # Avoids throttling bans on repeated failures
如果你倾向于写“做什么”的注释,通常是代码自身需要更清晰(更好命名、更小函数、更简单的控制流)。让代码承载事实;让注释承载推理。
没有什么比陈旧注释更快损害信任了。如果注释是可选的,它会随时间漂移;如果注释错误,它会成为活生生的 bug 来源。
一个实用习惯:把注释更新当作变更的一部分,而不是“可有可无”。在审查中,问一个合理的问题:这个注释还和行为一致吗? 如果不是,要么更新它,要么删掉它。“没有注释”总比“错误注释”好。
内联注释适合局部的惊讶点。更广泛的指导应放在 docstring、README 或开发者笔记中,尤其适用于:
一个好的 docstring 告诉人如何正确使用函数以及会遇到哪些错误,而不是叙述实现细节。短小的 /docs 或 /README 注释可以捕捉“为什么我们这样做”的故事,从而在重构时保存下来。
安静的胜利:更少的注释,但每条注释都物有所值。
大多数代码在“正常路径”上看起来“没问题”。品味的真实考验是在输入缺失、服务超时或用户做出意外操作时。处于压力下,聪明代码往往掩盖真相,而清晰的代码让失败显而易见且可恢复。
错误信息既是你产品的一部分,也是你调试流程的一部分。把它们写成给疲惫且正在值班的人看的样子。
应包含:
如果有日志,添加结构化上下文(如 requestId、userId 或 invoiceId),让消息在不翻查无关数据的情况下可操作。
有一种诱惑是用聪明的一行或通用的 catch-all 去“处理一切”。好品味是在重要的几种边界情况中进行选择并将其显式化。
例如,对于“空输入”或“未找到”做显式分支往往比一连串隐式产生 null 的变换更易读。当特殊情况重要时,给它命名并把它放到前面。
返回形态混杂(有时对象、有时字符串、有时 false)会迫使读者在脑中维护决策树。偏好保持一致的模式:
清晰的失败处理能减少惊讶——而惊讶正是 bug 与深夜告警的温床。
清晰不仅关乎你写代码时的意图,也关乎下一个人在 4:55pm 打开文件时期待看到的样子。一致性把“读代码”变成模式识别——更少惊讶、更少误解、更少在每个迭代周期重复出现的争论。
好的团队风格指南应简短、具体且务实。它不应试图编码每个偏好;而是解决经常出现的问题:命名约定、文件结构、错误处理模式以及测试的“完成”标准。
真正的价值在于社会层面:它阻止同样的讨论在每个新的 PR 中重置。当某件事被写下来,审查从“我偏好 X”转向“我们同意 X(以及为什么)”。让它保持活的并易于查找——许多团队将其置于仓库中(例如 /docs/style-guide.md),以便靠近代码。
使用格式化工具和 linter 来处理可测量且枯燥的规则:
这让人类能专注于意义:命名、API 形状、边缘情况,以及代码是否符合意图。
手动规则仍在当它们描述设计选择时发挥作用——例如“偏好早期返回以减少嵌套”或“每个模块只有一个公共入口点”。工具无法完全评判这些。
有时复杂是有正当理由的:严格的性能预算、嵌入式约束、棘手的并发或平台特定行为。约定应是:允许例外,但必须显式。
一个简单的标准能帮助:在注释中记录权衡、在性能被引用时添加微基准或测量,并把复杂代码隔离在清晰接口之后,使大部分代码库保持可读。
一次好的代码审查应更像一堂短小、集中的“好品味”课程,而不是一次检查。克尔尼汉的观点不是说聪明代码是邪恶的——而是当其他人要长期与之共处时,聪明是昂贵的。审查是让团队把这种权衡可见化并有意识选择清晰的地方。
先问:“队友能在一遍阅读中理解这段代码吗?”通常这意味着先看命名、结构、测试与行为,再深入微优化。
如果代码正确但难读,把可读性当作真实的缺陷。建议重命名变量以反映意图、拆分长函数、简化控制流,或添加一小段测试来展示期望行为。发现“能运行,但我看不出为何如此”的审查能防止未来几周的混乱。
一种实用的审查顺序:
当反馈以记分形式出现时,审查就会走偏。与其问“你为什么要这样?”,不如尝试:
问题邀请协作,通常还能暴露你不知道的约束。建议表达方向而不暗示不称职。这种语气是让“品味”在团队中传播的方式。
如果你想持续获得可读性,不要依赖审查者心情。在审查模板和完成定义中加入一些“清晰检查”,保持简短且具体:
随着时间推移,这会把审查从样式警察变成判断力教学——正是克尔尼汉所倡导的日常纪律。
大语言模型工具可以快速产出可运行代码,但“能运行”并不是克尔尼汉强调的标准——他强调的是“会交流”。如果你的团队使用基于对话生成代码的工作流程(例如通过聊天生成并迭代代码),值得把可读性作为第一类验收标准。
在像 Koder.ai 这样的产出平台上(你可以从聊天提示生成 React 前端、Go 后端和 Flutter 移动应用并导出源代码),同样的品味驱动习惯适用:
当产出仍然让人类容易审查、维护与扩展时,速度才最有价值。
清晰不是一次性“达到”的目标。代码只有在你不断把它往平易近人的方向推的时候才会保持可读。克尔尼汉的审美在这里很贴切:偏好稳健且可理解的渐进改进,而不是令人惊叹但次月让人迷惑的“聪明”单行写法。
最安全的重构是无聊的:微小的改动保持行为不变。每一步都跑测试。如果没有测试,添加一些聚焦检查作为临时护栏——这样你就能在不担心断行为的前提下改善结构。
一个实用节奏:
小提交也让代码审查更容易:队友能判断意图,而不是到处找副作用。
你不必一次性清除所有“聪明”构造。当你为特性或修复触及某段代码时,把聪明捷径换成直白等价物:
这就是在真实团队中赢得清晰的方式:在实际工作时逐一改善热点代码。
并非所有清理都是紧急的。一个有用的规则是:当代码正在被修改、经常被误解或可能引发 bug 时现在重构;当它稳定且孤立时则安排在后面。
让重构债务可见:留一个简短的 TODO 并带上上下文,或创建一个工单描述痛点(“难以添加新的支付方式;函数承担了 5 项职责”)。这样你可以被动决定——而不是让混乱代码悄悄变成团队永久的税。
克尔尼汉的影响力不在于 C 语言本身,而是一条持久的原则:代码是沟通的媒介。
语言和框架会变,但团队仍然需要容易扫描、推理、审查和排查的代码——尤其是在数月之后、在时间压力下。
“好品味”意味着持续选择最简单且清晰的方案来传达意图。
一个有用的测试是:团队成员能否回答“这段代码做了什么,以及为什么这么做?”而不需要解码技巧或依赖隐含假设。
因为大多数代码被阅读的频率远高于被编写的频率。
为读者优化能减少入职时间、审查摩擦和错误修改的风险——尤其是当维护者是带着更少上下文的“未来的你”时。
“聪明税”体现在:
如果聪明版本今天省下几秒,但每次触碰都要多花几分钟,那它就是净损失。
常见的祸根包括:
这些模式会隐藏中间状态,使审查时更容易漏掉边缘情况。
当它能减少认知负担时。
用具名变量把步骤显式化(比如 validate → normalize → compute)可以更容易验证正确性、简化调试并让将来改动更安全——即便它多了几行代码。
养成以下习惯:
invoiceTotalCents 替代 sum)如果你得解码一个名字,它就没做好本职工作;名字本该减少对注释的需求。
偏好简单、显式的分支,并保持“正常路径”可见。
通常有帮助的策略有:
注释要说明 为什么,而不是 做了什么。
好的注释记录意图、权衡或非显然的不变式,避免把明显的代码再重复一遍。将注释的更新视为变更的一部分——陈旧的注释比没有注释更糟。
用工具处理机械性的规则(格式化、导入顺序、明显的脚手架问题),把人工审查留给语义和意图问题。
简明的风格指南通过固定经常争论的点(命名、结构、错误处理模式)来减少反复争论。当确有例外(性能或约束)时,记录权衡并把复杂性封在干净的接口后面。
// Harder to scan
if (user \u0026\u0026 user.active \u0026\u0026 !user.isBanned \u0026\u0026 (role === 'admin' || role === 'owner')) {
allow();
}
// Clearer
if (!user) return deny('missing user');
if (!user.active) return deny('inactive');
if (user.isBanned) return deny('banned');
if (role !== 'admin' \u0026\u0026 role !== 'owner') return deny('insufficient role');
allow();