Skip to content

Kotlin 演进原则

实用演进原则

语言设计是铸就的,

但这块石头足够软,

稍加努力我们就可以在以后重塑它。

Kotlin 设计团队

Kotlin 旨在成为程序员的实用工具。就语言演进而言,其实用性体现在以下原则中:

  • 保持语言与时俱进。
  • 与用户保持持续的反馈循环。
  • 让用户更新到新版本变得轻松舒适。

由于这对于理解 Kotlin 如何发展前行至关重要,我们来阐述这些原则。

保持语言现代化。我们认识到系统会随着时间积累遗留代码。曾经的尖端技术今天可能会彻底过时。我们必须演进语言,使其符合用户需求并与他们的期望保持同步。这不仅包括添加新特性,还包括逐步淘汰不再推荐用于生产环境并已成为遗留特性的旧特性。

舒适的更新。不兼容变更,例如从语言中移除内容,如果处理不当,可能导致从一个版本到下一个版本的痛苦迁移。我们始终会提前充分宣布此类变更,将内容标记为已弃用,并在变更发生之前提供自动化迁移工具。到语言变更时,我们希望世界上大多数代码都已经更新,从而在迁移到新版本时没有问题。

反馈循环。经历弃用周期需要大量精力,因此我们希望最小化未来将进行的不兼容变更数量。除了运用我们最佳判断外,我们相信在实际中尝试是验证设计的最佳方式。在尘埃落定之前,我们希望它们经过实战检验。这就是为什么我们抓住每一个机会,在语言的生产版本中提供我们设计的早期版本,但这些版本处于预稳定状态之一:Experimental、Alpha 或 Beta。此类特性不稳定,随时可能更改,选择使用它们的用户会显式地表明他们已准备好处理未来的迁移问题。这些用户提供了宝贵的反馈,我们收集这些反馈以迭代设计并使其坚如磐石。

不兼容变更

如果从一个版本更新到另一个版本后,某些以前可以工作的代码现在不再工作,那么这就是语言中的_不兼容变更_(有时称为“破坏性变更”)。在某些情况下,“不再工作”具体意味着什么可能存在争议,但它肯定包括以下情况:

  • 以前编译和运行正常的代码现在因错误而被拒绝(在编译期或链接期)。这包括移除语言构造和增加新限制。
  • 正常执行的代码现在抛出异常。

属于“灰色地带”的不那么明显的情况包括以不同方式处理边缘情况、抛出与以前不同类型的异常、改变仅通过反射可观察到的行为、修改未文档化或未定义行为、重命名二进制构件等。有时此类变更至关重要并极大地影响迁移体验,有时则微不足道。

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

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

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

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

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

实际上,某些变更无法在编译期准确检测到,因此无法报告警告,但至少用户会通过版本 A 的发布说明被告知版本 B 即将到来变更。

处理编译器错误

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

决策

JetBrains 作为 Kotlin 的最初创建者,在社区的帮助下并与 Kotlin 基金会 合作推动其发展。

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

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

语言与工具发布

版本号如 2.0.0 的稳定发布版本通常被视为带来语言重大变更的_语言发布版本_。通常,我们会在语言发布版本之间发布版本号为 x.x.20 的_工具发布版本_。

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

语言发布版本通常会添加新特性,并可能移除或更改以前已弃用的特性。特性从预稳定到稳定的毕业也发生在语言发布版本中。

EAP 构建

在发布语言和工具发布版本的稳定版本之前,我们发布一些被称为 EAP(“抢先体验预览”的缩写)的预览构建,让我们更快地迭代并收集社区反馈。语言发布版本的 EAP 通常会生成二进制文件,这些文件随后将被稳定编译器拒绝,以确保二进制格式中可能存在的错误不会持续超过预览期。最终发布候选版本通常没有此限制。

预稳定特性

根据上述反馈循环原则,我们公开迭代我们的设计并发布语言版本,其中某些特性具有_预稳定_状态之一,并且_预计会发生变更_。此类特性可以在任何时候添加、更改或移除,且不发出警告。我们尽最大努力确保预稳定特性不会被不知情的用户意外使用。此类特性通常需要在代码中或项目配置中进行某种显式选择加入。

Kotlin 语言特性可以有以下状态之一:

  • 探索与设计。我们正在考虑将新特性引入语言。这涉及讨论它将如何与现有特性集成、收集用例以及评估其潜在影响。我们需要用户就此特性将解决的问题及其所针对的用例提供反馈。如果可能,估算这些用例和问题发生的频率也将是有益的。通常,想法会作为 YouTrack issue 记录下来,并在其中继续讨论。

  • KEEP 讨论。我们相当确定该特性应添加到语言中。我们旨在在一个名为 KEEP 的文档中提供动机、用例、设计和其他重要细节。我们期望用户反馈集中讨论 KEEP 中提供的所有信息。

  • 预览中。特性原型已准备就绪,您可以使用特性特定的编译器选项启用它。我们寻求您使用该特性的体验反馈,包括它如何轻松集成到您的代码库中、它如何与现有代码交互以及任何 IDE 支持问题或建议。该特特性的设计可能会发生显著变化,或可能根据反馈完全撤销。当特性处于 预览中 时,它具有一个 稳定性级别

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

  • 已撤销。我们已撤销该提案,将不会在 Kotlin 语言中实现该特性。如果不适合 Kotlin,我们可能会撤销处于 预览中 的特性。

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

不同组件的状态

了解更多关于 Kotlin 中不同组件的稳定性状态,例如 Kotlin/JVM、JS 和原生编译器,以及各种库。

语言离不开其生态系统,因此我们格外关注以实现平滑的库演进。

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

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

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

  • 库代码应始终显式指定公共/受保护函数和属性的返回类型,因此绝不依赖公共 API 的类型推断。类型推断的细微变化可能导致返回类型无意中改变,从而导致二进制兼容性问题。
  • 同一库提供的重载函数和属性应基本执行相同的操作。类型推断的变化可能导致在调用点已知更精确的静态类型,从而导致重载解析的更改。

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

此外,按照惯例,名为“internal”的包不被视为公共 API。位于名为“experimental”的包中的所有 API 都被视为预稳定,并且可以随时更改。

我们根据上述原则演进适用于稳定平台的 Kotlin 标准库 (kotlin-stdlib)。对其 API 契约的更改会经历与语言本身更改相同的程序。

编译器选项

编译器接受的命令行选项也属于一种公共 API,并且适用相同的考量。支持的选项(不带“-X”或“-XX”前缀的选项)只能在语言发布版本中添加,并且应在移除前妥善弃用。“-X”和“-XX”选项是实验性的,可以随时添加和移除。

兼容性工具

随着遗留特性被移除和错误被修复,源代码语言会发生变化,未正确迁移的旧代码可能无法再编译。正常的弃用周期为迁移提供了舒适的时间,即使周期结束并且变更随稳定版本发布,仍然有一种方法可以编译未迁移的代码。

兼容性选项

我们提供 -language-version X.Y-api-version X.Y 选项,使新版本能够模拟旧版本的行为以实现兼容性目的。为了给您更多迁移时间,除了最新的稳定版本外,我们还 支持 之前三个语言和 API 版本。

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

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

演进二进制格式

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

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

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

该协议旨在实现舒适的更新,因为即使项目使用的是略微过时的编译器,也不会被阻止更新其依赖项。

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

Kotlin klib 二进制文件

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

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

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