Kotlin 演进原则
务实演进原则
NOTE
语言设计是铸石,
但这块石头相当柔软,
稍加努力,我们日后便可重塑它。
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 通常会生成稍后会被稳定编译器拒绝的二进制文件,以确保二进制格式中可能存在的错误不会在预览期结束后继续存在。最终发布候选版 (RC) 通常没有此限制。
预稳定特性
根据上述“反馈循环”原则,我们公开迭代我们的设计,并发布语言版本,其中某些特性具有预稳定状态并预期会发生变化。此类特性可以随时添加、更改或移除,恕不另行通知。我们尽最大努力确保不知情的用户不会意外使用预稳定特性。此类特性通常需要在代码中或项目配置中进行某种形式的明确选择启用。
Kotlin 语言特性可以具有以下状态之一:
探索与设计。我们正在考虑向语言引入新特性。这涉及讨论它将如何与现有特性集成、收集用例以及评估其潜在影响。我们需要用户就此特性将解决的问题和其所处理的用例提供反馈。在可能的情况下,我们还会尝试估算这些用例和问题的发生频率,这将很有益。通常,想法会作为 YouTrack 问题记录下来,并在其中继续讨论。
KEEP 讨论。我们相当确定该特性应该添加到语言中。我们旨在在一份名为 KEEP 的文档中提供动机、用例、设计和其他重要细节。我们期望用户的反馈集中在讨论 KEEP 中提供的所有信息。
预览中。特性原型已准备就绪,您可以使用特定于特性的编译器选项启用它。我们寻求您使用该特性的体验反馈,包括它如何轻松地集成到您的代码库中,它如何与现有代码交互,以及任何 IDE 支持问题或建议。该特性的设计可能会根据反馈显著改变,或者可能会完全撤销。当一个特性处于预览中时,它具有一个稳定性级别。
稳定。该语言特性现在是 Kotlin 语言中的一等公民。我们保证其向后兼容性,并且我们将提供工具支持。
已撤销。我们已撤销该提案,将不会在 Kotlin 语言中实现该特性。如果某个处于预览中的特性不适合 Kotlin,我们可能会撤销它。
不同组件的状态
了解更多关于 Kotlin 中不同组件的稳定性状态,例如 Kotlin/JVM、JS 和 Native 编译器以及各种库。
库
没有生态系统,语言就什么也不是,因此我们格外关注以实现库的平稳演进。
理想情况下,新版本的库可以作为旧版本的“即插即用替代品”。这意味着升级二进制依赖不应破坏任何内容,即使应用程序未重新编译(这在动态链接下是可能的)。
一方面,为了实现这一点,编译器必须在分离编译的约束下提供一定的应用程序二进制接口 (ABI) 稳定性保证。这就是为什么语言中的每个更改都从二进制兼容性的角度进行检查的原因。
另一方面,很多方面取决于库作者是否谨慎选择哪些更改是安全的。因此,库作者理解源码更改如何影响兼容性,并遵循某些最佳实践以保持其库的 API 和 ABI 稳定至关重要。以下是我们从库演进角度考虑语言更改时做出的一些假设:
库代码应始终明确指定公共/受保护函数和属性的返回类型,从而绝不依赖类型推断来生成公共 API。类型推断中的细微更改可能会无意中导致返回类型发生变化,从而导致二进制兼容性问题。
同一库提供的重载函数和属性应本质上做相同的事情。类型推断中的更改可能导致在调用点已知更精确的静态类型,从而导致重载解析发生变化。
库作者可以使用 @Deprecated
和 @RequiresOptIn
注解来控制其 API 表面的演进。请注意,即使对于从 API 中移除的声明,也可以使用 @Deprecated(level=HIDDEN)
来保持二进制兼容性。
此外,按照惯例,命名为“internal”的包不被视为公共 API。所有位于命名为“experimental”的包中的 API 都被视为预稳定,并且可以随时更改。
我们根据上述原则演进用于稳定平台的 Kotlin 标准库(kotlin-stdlib
)。其 API 的契约更改遵循与语言本身更改相同的程序。
编译器选项
编译器接受的命令行选项也是一种公共 API,它们也受到相同考量。支持的选项(那些没有“-X”或“-XX”前缀的选项)只能在语言发布中添加,并且在移除之前应进行适当的弃用。“-X”和“-XX”选项是实验性的,可以随时添加和移除。
兼容性工具
随着遗留特性被移除和错误被修复,源语言会发生变化,未正确迁移的旧代码可能不再编译。正常的弃用周期提供了充裕的迁移时间,即使该周期结束并且变更在稳定版本中发布后,仍然有一种方法可以编译未迁移的代码。
兼容性选项
我们提供 -language-version X.Y
和 -api-version X.Y
选项,使新版本能够模拟旧版本的行为以实现兼容性。为了给您更多迁移时间,我们支持最新稳定版本以及前三个语言和 API 版本。
积极维护的代码库可以受益于尽快获得错误修复,而无需等待完整的弃用周期完成。目前,此类项目可以启用 -progressive
选项,即使在工具发布中也可以启用这些修复。
所有选项都可在命令行以及 Gradle 和 Maven 中使用。
演进二进制格式
与最坏情况下可以手动修复的源码不同,二进制文件更难迁移,这使得二进制文件的向后兼容性至关重要。对二进制文件不兼容的更改会使更新变得非常不舒适,因此应比源码语法中的更改引入得更加谨慎。
对于完全稳定版本的编译器,默认的二进制兼容性协议如下:
所有二进制文件都向后兼容;这意味着较新的编译器可以读取较旧的二进制文件(例如,1.3 可以理解 1.0 到 1.2 的二进制文件)。
较旧的编译器会拒绝依赖新特性的二进制文件(例如,1.0 编译器会拒绝使用协程的二进制文件)。
理想情况下(但我们不能保证),二进制格式与下一个语言版本大多向前兼容,但不能与后续版本兼容(在新特性未被使用的情况下,例如,1.9 可以理解 2.0 的大部分二进制文件,但不能理解 2.1 的)。
此协议旨在实现舒适更新,因为即使项目正在使用稍旧的编译器,也不会被阻止更新其依赖。
请注意,并非所有目标平台都已达到此稳定级别,但 Kotlin/JVM 已达到。
Kotlin klib 二进制文件
Kotlin klib 二进制文件在 Kotlin 1.9.20 中已达到稳定 (Stable) 级别。但是,您需要牢记一些兼容性细节:
从 Kotlin 1.9.20 开始,klib 二进制文件向后兼容。例如,2.0.x 编译器可以读取 1.9.2x 编译器生成的二进制文件。
不保证向前兼容性。例如,2.0.x 编译器不保证能够读取 2.1.x 编译器生成的二进制文件。
Kotlin cinterop klib 二进制文件仍处于 Beta 阶段。
目前,我们无法为不同 Kotlin 版本之间的 cinterop klib 二进制文件提供具体的兼容性保证。