13.3 Kotlin 演进原则
10 分钟阅读
原文链接: https://kotlinlang.org/docs/kotlin-evolution-principles.html
13.3 Kotlin 演进原则
务实演进的原则
语言设计如刻在石上, 但这块石头相当柔软, 稍加努力我们日后便能重塑它。 Kotlin 设计团队
Kotlin 被设计为程序员的务实工具。在语言演进方面,它的务实性体现在以下原则中:
- 让这门语言始终保持现代。
- 与用户保持持续的反馈循环。
- 让用户能够轻松、舒适地更新到新版本。
由于这是理解 Kotlin 如何前进的关键,我们来展开说明这些原则。
保持语言的现代性。我们认识到系统会随着时间累积历史包袱。曾经是尖端的技术,今天可能变得完全过时。我们必须不断演进这门语言,使其始终贴近用户的需求,并符合他们的期望。这不仅包括添加新特性,也包括逐步淘汰那些不再推荐用于生产、已经成为遗产的旧特性。
舒适的更新。不兼容变更(例如从语言中移除某些东西)如果处理不当,可能会导致从一个版本迁移到下一个版本的过程非常痛苦。我们始终会提前很久公布此类变更,将相关 API 标记为弃用,并在变更发生之前提供自动化迁移工具。等到语言真正发生变更时,我们希望世界上大多数代码都已经更新完毕,从而在迁移到新版本时不会遇到任何问题。
反馈循环。走完整个弃用周期需要付出大量努力,因此我们希望尽量减少未来做出的不兼容变更数量。除了依靠我们自己的最佳判断之外,我们认为在真实场景中试用是验证设计的最佳方式。在把事情刻进石头之前,我们希望它们经过实战检验。因此,我们会抓住一切机会,将设计的早期版本放到语言的正式版本中提供,但处于某种预稳定状态:实验性、Alpha 或 Beta。此类特性并不稳定,随时可能发生变化,选择使用它们的用户是明确表示自己愿意应对未来的迁移问题。这些用户提供了宝贵的反馈,我们据此迭代设计,使其牢不可破。
不兼容变更
如果在从一个版本更新到另一个版本后,某些原本可以正常工作的代码不再工作了,那就是语言中的不兼容变更(有时也被称为“破坏性变更”)。在某些情况下,“不再工作”的确切含义可能存在争议,但它肯定包括以下情况:
- 原本能正常编译和运行的代码现在被拒绝并报错(在编译期或链接期)。这包括移除语言结构和添加新的限制。
- 原本正常运行代码现在抛出异常。
属于“灰色地带”的不那么明显的情况包括:以不同方式处理边缘情况、抛出与之前不同类型的异常、改变只能通过反射观察到的行为、修改未文档化或未定义的行为、重命名二进制产物等等。有时这类变更至关重要,会极大地影响迁移体验;有时则无关紧要。
以下一些例子肯定不属于不兼容变更:
- 添加新的警告。
- 启用新的语言结构,或放宽对现有结构的限制。
- 修改 private/internal API 以及其他实现细节。
“保持语言的现代性”与“舒适的更新”这两条原则表明,不兼容变更有时是必要的,但应该谨慎引入。我们的目标是让用户提前很久就知晓即将发生的变更,以便他们从容地迁移代码。
理想情况下,每一项不兼容变更都应通过在有问题的代码处报告的编译期警告(通常称为弃用警告)来公布,并配有自动化的迁移辅助工具。因此,理想的迁移工作流如下:
- 更新到版本 A(在此版本中公布该变更)
- 看到关于即将发生的变更的警告
- 借助工具迁移代码
- 更新到版本 B(在此版本中变更发生)
- 发现完全没有问题
在实践中,有些变更无法在编译期被准确检测,因此无法给出警告,但至少用户会通过版本 A 的发布说明得知版本 B 中即将发生某项变更。
处理编译器缺陷
编译器是复杂的软件,尽管开发者尽了最大努力,它们仍然会有缺陷。导致编译器本身失败、报告虚假错误或生成明显无法运行代码的缺陷虽然令人烦恼、常常令人尴尬,但很容易修复,因为修复它们并不构成不兼容变更。其他缺陷可能导致编译器生成不正确但不会失败的代码:例如漏掉源代码中的某些错误,或者只是生成了错误的指令。修复这类缺陷在技术上属于不兼容变更(某些代码原本能正常编译,现在不能了),但我们倾向于尽快修复它们,以防错误的代码模式在用户代码中扩散。在我们看来,这符合“舒适的更新”原则,因为有机会遇到该问题的用户会更少。当然,这只适用于在正式版本中出现后很快就被发现的缺陷。
决策
Kotlin 的最初创造者 JetBrains 在社区的帮助下,并与 Kotlin 基金会合作,推动着 Kotlin 的发展。
Kotlin 编程语言的所有变更都由首席语言设计师(目前是 Michail Zarečenskij)监督。首席设计师在与语言演进相关的所有事务上拥有最终决定权。此外,对完全稳定组件的不兼容变更必须得到 Kotlin 基金会下设的语言委员会批准(目前成员包括 Jeffrey van Gogh、Werner Dietl 和 Michail Zarečenskij)。
语言委员会最终决定要实施哪些不兼容变更,以及应采取哪些具体措施让用户更新尽可能无缝。为此,委员会依据一套语言委员会指南。
语言特性的交付
如 Kotlin 发布流程中所述,语言特性通过语言版本(2.x.0)或其后续的工具版本(2.x.20)交付。
我们尽量让语言版本与工具版本彼此兼容,因此编译器中的变更大多是优化以及警告的新增/移除。预稳定特性可能随时被添加、移除或更改。
语言版本通常会添加新特性、把预稳定特性提升为稳定,并且可能移除或更改此前已弃用的特性。
EAP 构建
在发布语言版本和工具版本的稳定版之前,我们会发布若干预览构建,称为 EAP(即“Early Access Preview”,早期访问预览),以便更快地迭代并收集社区反馈。语言版本的 EAP 通常会产生随后会被稳定版编译器拒绝的二进制文件,以确保二进制格式中可能的缺陷不会存续超过预览期。RC2 或 RC3 等最终候选版本通常不受此限制。更多信息请参阅参与 Kotlin 早期访问预览。
预稳定特性
根据上文所述的“反馈循环”原则,我们在公开场合迭代设计,并发布语言中某些特性处于某种预稳定状态、本就预期会发生变化的版本。此类特性可以在任何时候、无需事先警告地被添加、更改或移除。我们会尽最大努力确保预稳定特性不会被毫无戒心的用户无意中使用。此类特性通常需要在代码或项目配置中以某种方式显式选择启用。
Kotlin 语言特性可以处于以下状态之一:
探索与设计。我们正在考虑为语言引入一项新特性。这包括讨论它将如何与现有特性集成、收集使用场景,以及评估其潜在影响。我们需要用户就这项特性将解决哪些问题、覆盖哪些使用场景提供反馈。只要可能,我们还会尝试估算这些使用场景和问题出现的频率。通常,想法会被记录为 YouTrack 问题,讨论也在那里继续进行。
KEEP 讨论。我们相当确定该特性应该被加入语言。我们会在名为 Kotlin Evolution and Enhancement Process(KEEP)的文档中提供动机、使用场景、设计及其他重要细节。我们希望用户的反馈聚焦于讨论 KEEP 中提供的所有信息。
预览中。特性的原型已经就绪,你可以使用该特性专用的编译器选项来启用它。我们希望获得你使用该特性的经验反馈,包括它集成到你的代码库中有多容易、它与现有代码的交互情况,以及任何 IDE 支持方面的问题或建议。该特性的设计可能会发生重大变化,也可能根据反馈被完全撤销。当特性处于预览中时,它具有实验性或 Beta 稳定性级别。
稳定。该语言特性现在是 Kotlin 语言中的一等公民。我们保证它的向后兼容性,并保证会提供工具支持。
已撤销。我们撤销了该提案,不会在 Kotlin 语言中实现该特性。如果某个预览中的特性不适合 Kotlin,我们可能会撤销它。
不同组件的状态
进一步了解 Kotlin 中不同组件的稳定性状态,例如 Kotlin/JVM、JS 和 Native 编译器,以及各种库。
库
没有生态系统的语言什么都不是,因此我们格外重视让库能够顺畅地演进。
理想情况下,库的新版本可以作为旧版本的“直接替换”。这意味着升级二进制依赖项不应破坏任何东西,即使应用程序没有被重新编译(在动态链接下这是可能的)。
一方面,为了实现这一点,编译器必须在分离编译的约束下提供某些应用二进制接口(ABI)稳定性保证。正因如此,语言中的每一项变更都会从二进制兼容性的角度加以审视。
另一方面,这在很大程度上取决于库作者是否谨慎对待哪些变更是安全的。因此,库作者必须理解源代码变更如何影响兼容性,并遵循某些最佳实践来保持其库的 API 和 ABI 稳定。以下是我们在从库演进角度考虑语言变更时所作的一些假设:
- 库代码应始终显式指定 public/protected 函数和属性的返回类型,从而绝不依赖类型推断来确定公共 API。类型推断中细微的变化可能会导致返回类型被无意间改变,进而引发二进制兼容性问题。
- 同一个库提供的重载函数和属性应该做本质上相同的事。类型推断的变化可能导致在调用点得知更精确的静态类型,从而改变重载解析的结果。
库作者可以使用 @Deprecated 和 @RequiresOptIn 注解来控制其 API 表面的演进。请注意,即使声明已从 API 中移除,也可以使用 @Deprecated(level=HIDDEN) 来保持二进制兼容性。
此外,按照约定,名为 “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。
为了给你更多迁移时间,在 JVM 上,除了最新的稳定版本之外,我们还支持至少三个先前的语言版本和 API 版本。这让库作者可以在采用更新的编译器版本的同时,仍与使用较旧编译器版本的使用方保持兼容。在其他平台上,你也可以配置较旧的语言版本和 API 版本,但与 JVM 不同,使用方仍然需要使用最新的编译器版本。
在大多数项目中,请把这两个选项设置为相同的版本。较低的 API 版本主要在你需要与较旧版本的 Kotlin 标准库保持兼容时才有用。
积极维护的代码库可以受益于尽快获得缺陷修复,而无需等待完整的弃用周期结束。这些项目可以启用 -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 中达到了稳定级别。不过,有一些兼容性细节你需要牢记:
- 从 Kotlin 1.9.20 开始,klib 二进制文件向后兼容。例如,2.0.x 编译器可以读取由 1.9.2x 编译器生成的二进制文件。
- 向前兼容性不受保证。例如,2.0.x 编译器不保证能读取由 2.1.x 编译器生成的二进制文件。
注意: Kotlin cinterop klib 二进制文件仍处于 Beta 阶段。目前,我们无法对 cinterop klib 二进制文件在不同 Kotlin 版本之间的兼容性给出具体保证。