Skip to content

Kotlin 演进原则

务实演进的原则

“语言设计一旦确定便坚如磐石,

但这块石头尚算柔软,

经过一番努力,我们稍后仍可对其重塑。”

—— Kotlin 设计团队

Kotlin 被设计为一种面向程序员的务实工具。在语言演进方面,其务实性体现在以下原则中:

  • 随时间推移保持语言的现代性。
  • 与用户保持持续的反馈循环。
  • 使升级到新版本对用户而言既简单又舒适。

这是理解 Kotlin 如何向前发展的关键,让我们展开说明这些原则。

保持语言的现代性。我们意识到系统会随着时间的推移而积累遗留内容。曾经的前沿技术在今天可能已经无可救药地过时了。我们必须演进语言,使其始终符合用户的需求并紧跟他们的预期。这不仅包括添加新功能,还包括逐步淘汰那些不再推荐用于生产环境并已成为遗留负担的旧功能。

舒适的更新。如果不加妥善处理,移除语言功能等不兼容更改可能会导致从一个版本迁移到下一个版本的过程非常痛苦。我们始终会提前很久宣布此类更改,将相关内容标记为弃用,并在“更改发生之前”提供自动迁移工具。当语言发生变化时,我们希望世界上大部分代码已经完成了更新,从而能够毫无问题地迁移到新版本。

反馈循环。经历弃用周期需要付出巨大的努力,因此我们希望尽量减少未来进行不兼容更改的次数。除了凭借我们的专业判断力外,我们相信在现实生活中进行尝试是验证设计的最佳方式。在将其“坚如磐石”地确定下来之前,我们希望它们经过实战测试。这就是为什么我们会利用一切机会,在语言的生产版本中提供设计的早期版本,但将其置于某种“预稳定”状态:Experimental、Alpha 或 Beta。此类功能并不稳定,随时可能更改,选择使用这些功能的用户的明确意图是他们已经准备好应对未来的迁移问题。这些用户提供了极其宝贵的反馈,我们收集这些反馈来迭代设计并使其坚如磐石。

不兼容更改

如果在从一个版本更新到另一个版本时,原本可以运行的代码不再能运行,那么这就是语言中的“不兼容更改”(有时也称为“破坏性更改”)。关于在某些情况下“不再能运行”的具体含义可能存在争议,但它肯定包括以下内容:

  • 原本可以正常编译运行的代码现在被报错拒绝(在编译时或链接时)。这包括移除语言构造和添加新的约束。
  • 原本可以正常执行的代码现在会抛出异常。

属于“灰色地带”的较不明显的情况包括:对边缘情况的处理方式不同、抛出与之前不同类型的异常、更改仅通过反射可见的行为、修改未记录或未定义的行为、重命名二进制构件等。有时此类更改至关重要,会极大地影响迁移体验,有时它们则微不足道。

肯定不属于不兼容更改的示例包括:

  • 添加新的警告。
  • 启用新的语言构造或放宽对现有构造的限制。
  • 更改私有/内部 API 和其他实现细节。

保持语言现代性和舒适更新的原则表明,不兼容更改有时是必要的,但必须谨慎引入。我们的目标是让用户提前了解即将发生的更改,以便他们舒适地迁移代码。

理想情况下,每个不兼容更改都应通过在有问题的代码中报告编译时警告(通常称为“弃用警告”)来宣布,并伴随自动迁移辅助工具。因此,理想的迁移工作流如下:

  • 更新到版本 A(宣布更改的版本)
    • 看到关于即将发生更改的警告
    • 在工具的帮助下迁移代码
  • 更新到版本 B(发生更改的版本)
    • 完全没有问题

在实践中,某些更改无法在编译时准确检测,因此无法报告警告,但至少会通过版本 A 的发布说明通知用户版本 B 将发生更改。

处理编译器错误 (bug)

编译器是复杂的软件,尽管开发者已尽最大努力,但仍会存在错误。那些导致编译器自身崩溃、报告虚假错误或生成明显失败代码的错误,虽然令人恼火且往往令人尴尬,但很容易修复,因为修复并不构成不兼容更改。其他错误可能会导致编译器生成不会失败但错误的代码:例如,漏掉了源代码中的某些错误,或者只是生成了错误的指令。对此类错误的修复在技术上是不兼容更改(某些代码以前编译正常,但现在不行了),但我们倾向于尽快修复它们,以防止错误的代码模式在用户代码中蔓延。我们认为这符合舒适更新的原则,因为这样更少的用户会有机会遇到该问题。当然,这仅适用于在发布版本中出现后很快就被发现的错误。

决策制定

Kotlin 的最初创建者 JetBrains 在社区的帮助下并与 Kotlin Foundation 协作,推动着它的进步。

Kotlin 编程语言的所有更改均由 首席语言设计师(目前为 Michail Zarečenskij)监督。首席设计师在所有与语言演进相关的事项上拥有最终决定权。此外,针对完全稳定组件的不兼容更改必须由 Kotlin Foundation 指定的 语言委员会 批准(目前成员包括 Jeffrey van Gogh、Werner Dietl 和 Michail Zarečenskij)。

语言委员会就将进行哪些不兼容更改以及应采取哪些确切措施来使用户更新尽可能无缝做出最终决定。在此过程中,它依赖于一套语言委员会指南

语言和工具版本发布

具有版本号(如 2.0.0)的稳定版本通常被视为“语言版本”,会带来语言的重大变化。通常,我们在语言版本之间发布“工具版本”,编号为 x.x.20

工具版本会带来工具的更新(通常包括功能)、性能改进和错误修复。我们尽量保持这些版本之间的兼容性,因此对编译器的更改主要是优化和警告的添加/移除。预稳定功能可能随时被添加、移除或更改。

语言版本经常添加新功能,并可能移除或更改之前弃用的功能。功能从预稳定阶段到稳定阶段的过渡也会在语言版本中发生。

EAP 构建

在发布语言和工具的稳定版本之前,我们会发布许多名为 EAP(即“Early Access Preview”,抢先体验预览)的预览构建,以便我们更快地迭代并收集社区反馈。语言版本的 EAP 通常会生成稍后会被稳定编译器拒绝的二进制文件,以确保二进制格式中可能存在的错误不会超出预览期。最终的 Release Candidate(发布候选版)通常不受此限制。要了解更多信息,请参阅参与 Kotlin 抢先体验计划

预稳定功能

根据上述反馈循环原则, we 公开迭代我们的设计,并发布某些功能处于“预稳定”状态且“理应更改”的语言版本。此类功能可能在任何时间被添加、更改或移除,且不另行通知。我们尽最大努力确保普通用户不会意外使用到预稳定功能。此类功能通常需要在代码或项目配置中进行某种显式的开启 (opt-in)。

Kotlin 语言功能可以处于以下状态之一:

  • 探索与设计 (Exploration and design):我们正在考虑向语言引入一项新功能。这包括讨论它将如何与现有功能集成、收集用例以及评估其潜在影响。我们需要用户对该功能将解决的问题和所应对的用例提供反馈。如果可能,估算这些用例和问题的发生频率也会非常有益。通常,想法会被记录为 YouTrack 问题并在此继续讨论。

  • KEEP 讨论 (KEEP discussion):我们相当确定该功能应该被添加到语言中。我们的目标是在名为 KEEP 的文档中提供动机、用例、设计和其他重要细节。我们期望用户的反馈集中在讨论 KEEP 中提供的所有信息上。

  • 预览中 (In preview):功能原型已就绪,你可以使用特定于功能的编译器选项启用它。我们寻求关于你使用该功能体验的反馈,包括它集成到代码库的难易程度、它如何与现有代码交互,以及任何 IDE 支持问题或建议。该功能的设计可能会发生重大变化,或者根据反馈被完全撤销。当功能处于“预览中”时,它具有 Experimental 或 Beta 稳定性级别

  • 稳定 (Stable):该语言功能现在是 Kotlin 语言的一等公民。我们保证其向后兼容性,并提供工具支持。

  • 已撤销 (Revoked):我们已撤销该提案,将不会在 Kotlin 语言中实现该功能。如果处于“预览中”的功能不适合 Kotlin,我们也可能会撤销它。

查看 Kotlin 语言提案及其状态的完整列表

不同组件的状态

详细了解 Kotlin 中不同组件的稳定性状态,例如 Kotlin/JVM、JS 和 Native 编译器,以及各种库。

没有生态系统的语言是毫无意义的,因此我们格外关注如何实现库的平滑演进。

理想情况下,库的新版本可以作为旧版本的“直接替代品”使用。这意味着升级二进制依赖项不应破坏任何内容,即使应用程序没有重新编译(这在动态链接下是可能的)。

一方面,为了实现这一点,编译器必须在独立编译的约束下提供某些“应用程序二进制接口”(ABI)稳定性保证。这就是为什么语言中的每一项更改都会从二进制兼容性的角度进行检查。

另一方面,很大程度上取决于库作者对哪些更改是安全的保持谨慎。因此,库作者理解源代码更改如何影响兼容性,并遵循某些最佳做法来保持其库的 API 和 ABI 稳定,这一点至关重要。以下是我们在从库演进角度考虑语言更改时做出的一些假设:

  • 库代码应始终显式指定公共/受保护函数和属性的返回值类型,从而绝不依赖公共 API 的类型推断。类型推断的细微变化可能会导致返回值类型无意中发生变化,从而导致二进制兼容性问题。
  • 由同一个库提供的重载函数和属性本质上应该做同样的事情。类型推断的变化可能会导致在调用站点获知更精确的静态类型,从而导致重载决策的变化。

库作者可以使用 @Deprecated@RequiresOptIn 注解来控制其 API 暴露面的演进。请注意,@Deprecated(level=HIDDEN) 可用于为已从 API 中移除的声明保留二进制兼容性。

此外,按照约定,名为 “internal” 的软件包不被视为公共 API。所有位于名为 “experimental” 的软件包中的 API 都被视为预稳定状态,随时可能发生变化。

我们根据上述原则为稳定平台演进 Kotlin 标准库 (kotlin-stdlib)。其 API 契约的更改须遵循与语言本身更改相同的程序。

编译器选项

编译器接受的命令行选项也是一种公共 API,它们也受到相同的考量。支持的选项(那些没有 “-X” 或 “-XX” 前缀的选项)只能在语言版本中添加,并且在移除之前应进行适当的弃用。 “-X” 和 “-XX” 选项是实验性的,可以随时添加和移除。

兼容性工具

随着遗留功能的移除和错误的修复,源代码语言会发生变化,未正确迁移的旧代码可能无法再编译。正常的弃用周期为迁移提供了充足的时间,即使弃用期结束且更改在稳定版本中发布,仍然有办法编译未迁移的代码。

兼容性选项

我们提供兼容性选项,使新的 Kotlin 版本能够为了兼容性而模拟旧版本的行为:

  • -language-version X.Y - Kotlin 语言版本 X.Y 的兼容模式,对之后出现的所有语言功能报告错误。
  • -api-version X.Y - Kotlin API 版本 X.Y 的兼容模式,对所有使用 Kotlin 标准库中较新 API 的代码(包括编译器生成的代码)报告错误。

为了给你留出更多时间进行迁移,除了最新的稳定版本外,我们还支持至少三个之前的语言和 API 版本的开发。

积极维护的代码库可以受益于尽快获得错误修复,而无需等待完整的弃用周期完成。目前,此类项目可以启用 -progressive 选项,即使在工具版本中也能启用此类修复。

所有选项都可以在 IDE、命令行以及 GradleMaven 中使用。

演进二进制格式

源代码在最坏的情况下可以手动修复,但二进制文件迁移起来要困难得多,这使得向后兼容性在二进制文件的情况下至关重要。二进制文件的不兼容更改会使更新变得非常不舒适,因此引入时应比源代码语法中的更改更加谨慎。

对于完全稳定版本的编译器,默认的二进制兼容性协议如下:

  • 所有二进制文件都是向后兼容的;这意味着较新的编译器可以读取较旧的二进制文件(例如,1.3 可以理解 1.0 到 1.2 版本)。
  • 较旧的编译器会拒绝依赖新功能的二进制文件(例如,1.0 编译器会拒绝使用协程的二进制文件)。
  • 优选情况下(但我们不能保证),二进制格式大多与下一个语言版本向前兼容,但不兼容更晚的版本(在未使用新功能的情况下,例如 1.9 可以理解 2.0 的大多数二进制文件,但不能理解 2.1 的)。

该协议专为舒适更新而设计,因为即使某个项目使用的是略微过时的编译器,也不会阻碍其更新依赖项。

请注意,并非所有目标平台都达到了这种稳定水平,但 Kotlin/JVM 已经达到了。

Kotlin klib 二进制文件

Kotlin klib 二进制文件在 Kotlin 1.9.20 中已达到 稳定 级别。但是,你需要记住一些兼容性细节:

  • 从 Kotlin 1.9.20 开始,klib 二进制文件向后兼容。例如,2.0.x 编译器可以读取由 1.9.2x 编译器生成的二进制文件。
  • “不”保证向前兼容性。例如,2.0.x 编译器“不”保证能读取由 2.1.x 编译器生成的二进制文件。

Kotlin cinterop klib 二进制文件仍处于 Beta 阶段。 目前,我们无法为不同 Kotlin 版本之间的 cinterop klib 二进制文件提供具体的兼容性保证。