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(代表“Early Access Preview”),这使我们可以更快地迭代并收集来自社区的反馈。语言发布的 EAP 通常会生成二进制文件,这些二进制文件稍后会被稳定的编译器拒绝,以确保二进制格式中可能存在的错误不会超过预览期。最终的候选发布版本通常没有此限制。
预稳定功能
根据上面描述的反馈循环原则,我们公开迭代我们的设计,并发布一些功能具有_预稳定_状态之一并且_应该更改_的语言版本。此类功能可以随时添加、更改或删除,恕不另行通知。我们尽最大努力确保不知情的用户不会意外使用预稳定功能。此类功能通常需要在代码或项目配置中进行某种形式的显式选择加入。
Kotlin 语言功能可以具有以下状态之一:
-
探索和设计。我们正在考虑向该语言引入一项新功能。这包括讨论它将如何与现有功能集成、收集用例以及评估其潜在影响。我们需要用户对该功能将解决的问题及其解决的用例的反馈。在可能的情况下,我们尝试估计这些用例和问题的发生频率也会有所帮助。通常,这些想法记录为 YouTrack 问题,讨论将在其中继续。
-
KEEP 讨论。我们非常确定应将该功能添加到该语言中。我们的目标是在名为_KEEP_的文档中提供动机、用例、设计和其他重要细节。我们希望用户提供的反馈侧重于讨论 KEEP 中提供的所有信息。
-
预览中。功能原型已准备就绪,您可以使用特定于功能的编译器选项来启用它。我们寻求有关您使用该功能的体验的反馈,包括它如何轻松地集成到您的代码库中、它如何与现有代码交互以及任何 IDE 支持问题或建议。该功能的设计可能会发生重大变化,或者可能会根据反馈完全撤销。当一个功能处于_预览中_时,它具有稳定性级别。
-
稳定。该语言功能现在是 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”选项是实验性的,可以随时添加和删除。
兼容性工具
随着旧功能的删除和错误的修复,源语言发生变化,并且未正确迁移的旧代码可能无法再编译。正常的弃用周期允许有足够的时间进行迁移,即使该周期已经结束并且更改已在稳定版本中发布,仍然有一种方法可以编译未迁移的代码。
兼容性选项
我们提供 -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 中达到 稳定 级别。但是,您需要牢记一些兼容性细节:
- 从 Kotlin 1.9.20 开始,klib 二进制文件是向后兼容的。例如,2.0.x 编译器可以读取由 1.9.2x 编译器生成的二进制文件。
- 不保证向前兼容性。例如,不保证 2.0.x 编译器可以读取由 2.1.x 编译器生成的二进制文件。
Kotlin cinterop klib 二进制文件仍处于 Beta 阶段。目前,我们无法为 cinterop klib 二进制文件的不同 Kotlin 版本之间提供具体的兼容性保证。