
Kotlin的设计哲学之:弃用标记
Kotlin的设计哲学之:弃用标记
JetBrains的工程师们,是我见过最优雅、最具有工程师精神的一批工程师。
我这句话先撂在这里,看谁来反驳。
一、被注释掉的代码,是项目里的定时炸弹
我接手过一个项目。整个工程只有一个类文件,一万多行,全局变量散落各处,命名随意到像是在键盘上乱敲的,if-else 嵌套深不见底。一行注释没有,但被注释掉的代码有整整八百行:
800行被注释的代码:

众多意义不明的全局变量们:

单文件一万行的项目:

我没有在屎山上继续堆砌,而是花了一周把整个项目的逻辑和功能摸透,补上注释,然后推翻重写。
我问同事:那些被注释的代码是什么意思?是准备之后恢复,还是可以直接删除?
同事说:太久了,我也不记得了。但不删吧——万一有用呢。
这就是问题。
被注释掉的代码,本身没有恶意。写它的那个程序员可能只是暂时跳过了某个功能,可能只是调试时随手关掉了一段逻辑。但项目一交接、时间一拉长,它就变成了无人能解的谜:能动吗?删了会出事吗?恢复后还能跑吗?
注释在一开始是注释,在后来者眼里就是地雷。
但本文不展开讲命名、讲架构、讲那些更大的话题。本文只聚焦一个点:当你需要废弃一个函数、或者标记一段未完成的代码时,正确的做法是什么。
二、只有 JetBrains 在乎
在回答"怎么做"之前,先看看各家 IDE 对这件事的态度。
用 Visual Studio 写 C#和c++,代码里注释掉几百行,IDE 不但不警告,还会在左下角贴个绿色标记——"未找到相关问题":
VS提示未找到问题:
VS Code 也一样,默认毫无反应:

我用过的众多 IDE 里,只有 IntelliJ IDEA 会对你被注释的代码发出黄标警告:

刚开始我觉得这群 JetBrains 工程师真是多管闲事——我就注释几行代码,至于吗?
后来项目经得多了,我才明白他们在做什么。被注释掉的代码,刚写下的时候没什么大不了;但项目变大、人换了又换,后来者看到这段代码,它就成了一道迷题:它到底是暂时停用,还是永久废弃,还是干脆是一个忘了删除的旧版本? 没人知道。被注释的代码不会说话,但它留下的困惑,足以让维护者抓狂。
是的,他们用心良苦,为了防止你们写出不规范的代码,他们甚至想要手把手教你。
这就是 JetBrains 这家公司的特质。这里需要交代一个背景:Kotlin 这门语言本身就是 JetBrains 设计的。他们同时也是 IntelliJ IDEA 的创造者。所以你在 Kotlin 里看到的语言特性,和在 IDEA 里看到的代码警告,全部出自同一批工程师之手。理解了这一点——他们既设计语言又设计工具——你才能理解后面每一个设计决策背后的逻辑:语言和工具的配合,他们做到了天衣无缝。语言强制你写清楚,IDE 帮你看到、帮你改。后面你会反复看到这种组合拳。

那回到问题本身:一段代码不想留、又不能直接删,应该怎么标记?一段还没写完的功能,应该怎么占位?
答案分别是 弃用注解 和 TODO 标记。下面逐个展开。
三、使用弃用注解
3.1 各语言弃用标记对比
弃用标记不是 Kotlin 的专利。几乎所有主流语言都提供了这个机制,只是形态各不相同:
| 语言 | 语法形态 | 提示信息 | 替代方案 |
|---|---|---|---|
| Java | @Deprecated 注解 | 可选 —— @Deprecated 可以什么都不填 | 无原生支持 |
| C# | [Obsolete] 特性 | 可选 —— [Obsolete] 无参也能用 | 无原生支持 |
| Python | @deprecated 装饰器(PEP 702 / warnings 模块) | 可选 | 无原生支持 |
| JavaScript / TypeScript | /** @deprecated */ JSDoc 注释 | 可选 —— 纯注释,不写也不报错 | 无原生支持 |
| C++ | [[deprecated]] 特性(C++14) | 可选 —— [[deprecated]] 无参合法 | 无原生支持 |
| Rust | #[deprecated] 属性 | 可选 —— #[deprecated] 无参合法 | 有 since / note 字段,但均可选 |
看出规律了没有?家家都有,但提示信息全是可选的。这意味着什么?意味着任何一个偷懒的程序员,都可以写出这样的代码:
// Java — 编译通过,一声不吭
@Deprecated
public void oldMethod() { }
// C# — 编译通过,什么提示都没有
[Obsolete]
public void OldMethod() { }
// C++ — 编译通过,没有任何说明
[[deprecated]]
void oldFunction();
// JavaScript / TypeScript — 编译通过,不写 JSDoc 也没人管
/** @deprecated */
function oldFunction() { }调用者收到一个孤零零的弃用警告,但不知道为什么被弃用、应该换成什么。这和把代码注释掉留一句 // 不要用 没有本质区别——前一节的屎山,就是这样一铲一铲堆起来的。
3.2 这就是 JetBrains 工程师的优雅
来看看 Kotlin 的设计。它的 @Deprecated 注解定义:
// Kotlin 标准库中的实际定义
@Target(allowedTargets = [CLASS, FUNCTION, PROPERTY, ...])
public annotation class Deprecated(
val message: String, // ← 注意:String,不是 String?
val replaceWith: ReplaceWith = ReplaceWith(""),
val level: DeprecationLevel = DeprecationLevel.WARNING
)看到没?message 的类型是 String,不是 String?。这意味着你必须提供一段文字说明,不能留空,不能传 null。下面这样的代码,直接编译不过:
@Deprecated // ❌ 编译错误:缺少必填参数 message
@Deprecated("") // ❌ 编译警告:空字符串仍然不提供任何信息
@Deprecated("请使用 newFunction() 替代") // ✅ 这才是正确做法
fun oldFunction() { }
对比太鲜明了:
| 语言 | 空参数 | 效果 |
|---|---|---|
| Java | @Deprecated | ✅ 编译通过,没人知道原因 |
| C# | [Obsolete] | ✅ 编译通过,零信息 |
| C++ | [[deprecated]] | ✅ 编译通过,无任何说明 |
| JavaScript / TypeScript | /** @deprecated */ | ✅ 编译通过,不写 JSDoc 也没人管 |
| Kotlin | @Deprecated | ❌ 编译失败,强迫你写原因 |
这不是语法糖,这是设计哲学。JetBrains 的工程师们直接在编译器层面堵死了这个漏洞——你想废弃一个函数?可以,但你必须告诉后来人为什么、换什么。他们宁可让你多敲一行 message,也不允许项目里出现一个"不知道为什么被弃用"的函数。
这就是工程师精神:用编译器的强制力,把坏习惯从语法层面消灭掉。
3.3 ReplaceWith:不仅告诉你"别用",还告诉你"用这个"
message 是强制你解释为什么,而 ReplaceWith 更进一步——直接给出迁移方案:
@Deprecated(
message = "此方法已废弃,请使用 calculateV2()",
replaceWith = ReplaceWith("calculateV2(a, b)"),
level = DeprecationLevel.WARNING
)
fun calculateV1(a: Int, b: Int): Int {
return calculateV2(a, b)
}调用者通过 Alt + Enter → Replace with calculateV2(a, b) 一键自动重构,IDE 帮你改代码,不是给你丢一句话自己琢磨。
3.4 DeprecationLevel:渐进式删除的工业级方案
JetBrains 的工程师显然经历过大型项目的 API 演进——光有 message 和 ReplaceWith 还不够,你需要控制"废弃到什么程度":
| 等级 | 编译效果 | 调用者感受 | 适用阶段 |
|---|---|---|---|
WARNING | 黄色警告,编译通过 | "这个旧了,但你还能用" | 刚刚废弃,给缓冲期 |
ERROR | 红色错误,编译失败 | "必须换,除非你显式 @Suppress" | 限期迁移 |
HIDDEN | 对外部完全不可见 | "这个东西不存在了" | 最终移除 |
实战中一个完整的弃用生命周期:
class ApiClient {
// v2.0:警告期,提醒调用方有新版
@Deprecated(
message = "fetchUsers() 已废弃,请迁移至 suspend 版的 fetchUsersAsync()",
replaceWith = ReplaceWith("fetchUsersAsync()"),
level = DeprecationLevel.WARNING
)
fun fetchUsers(): List<User> { ... }
// 新的方法
suspend fun fetchUsersAsync(): List<User> { ... }
}v2.5 升 ERROR,v3.0 升 HIDDEN。整个过程调用者被逐步引导,不会被突然破坏——够不够工业?
仔细想想:
message强非空 → 强制你给理由;ReplaceWith→ 给出迁移路径;DeprecationLevel三级 → 分阶段执行。三件套环环相扣,没有一个步骤是多余的。这就是JetBrains工程师的优雅——不是给你一个工具,而是给你一套完整的、工业级的解决方案。
四、使用 TODO 标记
弃用注解解决了"旧东西怎么删"。反过来——新功能还没写完,怎么标记?
4.1 各语言的 TODO 支持情况
大部分语言把"未完成的代码"这件事完全交给了注释来办——语言层面什么都不提供:
| 语言 | TODO 机制 | 类型 |
|---|---|---|
| Java | // TODO 注释约定 | 纯注释,编译器完全不管 |
| C# | // TODO 注释约定 | 纯注释,VS 会在任务列表中收集 |
| Python | # TODO 注释约定 | 纯注释 |
| JavaScript | // TODO 注释约定 | 纯注释 |
| C / C++ | // TODO 注释约定 | 纯注释 |
| Rust | todo!() 宏 | 语言级,运行时 panic,和 Kotlin 思路接近 |
绝大多数语言,TODO 只是一行对人类无效的注释——同事不会天天翻你的注释看。Rust 的 todo!()宏思路接近, Kotlin 则是把它做成标准库函数 + Nothing 返回,且 IDE 对 TODO() 有额外工具链支持(TODO 面板、跳转)。
4.2 Kotlin 的答案:TODO()
Kotlin 是怎么做的?它直接把 TODO 做成了标准库函数,返回值类型是 Nothing:
fun calculateDiscount(price: Double): Double {
TODO("根据用户等级计算折扣,待业务方确认规则")
}它的实现只有一行:
public inline fun TODO(reason: String): Nothing =
throw NotImplementedError("An operation is not implemented: $reason")注意这个 reason: String ——又是非空 String,不是 String?。你必须写清楚为什么这里还没完成,不能丢一个空的 TODO() 就跑。
4.3 优雅在哪
用注释标记未完成代码,有两个致命的坑:
- 编译期完全静默:
// TODO和普通注释没有任何区别,编译器一视同仁地忽略 - 运行时悄悄跳过:你如果注释掉了一整段逻辑,代码路径直接消失——可能返回一个默认值、一个
null,鬼知道上线后会发生什么
Kotlin 的 TODO() 是反向思路:
| 做法 | 编译 | 运行时表现 |
|---|---|---|
// TODO 注释掉 | ✅ 通过 | 静默跳过逻辑,返回值不确定 |
TODO("原因") | ✅ 通过 | 直接崩溃,栈回溯精确指向未完成的代码位置 |
崩溃不是 bug——崩溃是你故意埋的保险丝。它强迫你在上线前面对那段没写完的代码,而不是假装它已经完成了。你在开发环境忘了,TODO() 会在你第一次点到这里时用 NotImplementedError 把你炸回去。
反过来想想:一个不崩溃、悄悄返回默认值的半成品函数,和一个一调用就崩溃、带着明确错误信息和栈回溯的函数——哪个更容易在代码审查中被发现?哪个更容易在生产环境漏过去?
JetBrains 的工程师把"未完成"从注释提升到了语言特性。其他语言里 TODO 是一个团队规范,记不记、写不写全靠自觉;Kotlin 里,TODO() 是一个保证——编译通过,但运行就跑不掉的保证。
4.4 // TODO 注释 + IDEA TODO 面板
除了函数级的 TODO(),Kotlin 也兼容传统的 // TODO 注释。但 JetBrains 的工具链没有停留在"把注释高亮一下"——IDEA 有一个全局的 TODO 工具窗口(View → Tool Windows → TODO),所有 // TODO、// FIXME 自动被收入一张统一清单。
// TODO: 改用协程替换 Thread.sleep
// FIXME: 此处数据库连接的线程安全问题
// TODO(user): 确认国际化的语言列表,目前只有中英文可以按关键词、文件范围、作者标签筛选。同事离职交接时,打开这个窗口,所有遗留问题一目了然——不需要翻遍文件去回忆"好像哪里没写完"。


4.5 组合使用
@Deprecated 和 TODO() 刚好互补,覆盖代码从生到死的全过程:
// 旧函数:告诉后来人这个要删了
@Deprecated(
message = "v2.0 起废弃,请迁移至 processOrderV2()",
replaceWith = ReplaceWith("processOrderV2(order)"),
level = DeprecationLevel.WARNING
)
fun processOrder(order: Order) { ... }
// 新函数:未完成的步骤用 TODO() 占位
fun processOrderV2(order: Order) {
// ...
TODO("待对接支付接口后完成")
}一个管"旧的怎么退场",一个管"新的怎么占位"——都带强制的、非空的说明文字。
总结
回到开头那句话:JetBrains 的工程师们,是我见过最优雅、最具有工程师精神的一批工程师。
他们设计的弃用标记体系,从头到尾只在做一件事:让代码的意图被准确表达,并且用编译器强制你做到。
两个设计决策贯穿全文:
@Deprecated的message是强制的、非空的。 其他语言让你随意——想写就写,不想写留空也行。Kotlin直接堵死这个洞:你不写?编译不过。你必须给后来人一个交代。这和把代码注释掉留一句模糊的话相比,是"确定性"对"不确定性"的碾压。TODO()是一个会崩溃的函数,不是一行注释。 其他语言用// TODO注释来标记"未完成",编译器不理它,运行时也默默跳过。Kotlin把 TODO 做成了语言标准库的一等公民,返回值是Nothing,原因参数必填非空。你不完成它?它就在运行时把你炸回去。
这就是我说的"工业级"。不是给你一个用不用都行的建议,而是用编译器和类型系统确保你必须这样做。
下次你想注释掉一段代码时,停一秒钟:旧的该废弃就用 @Deprecated(带上非空的理由),没写完的用 TODO()(带上非空的原因),而不是留下一地注释让后来人猜。
这就是工程师的精神:严谨,有一种完美主义精神,用强制性来规范使用者做出正确的事。
