本文首次写于 2024年5月28日,编辑于 2024年5月30日,根据主题演讲新增大量内容,并调整部分内容
几天前的 Kotlin Conf'24 的主题演讲上,JetBrains 带来了诸多更新。除了备受关注的 Kotlin 2.0.0 稳定版(What's new in Kotlin 2.0.0 | Kotlin Documentation (kotlinlang.org))发布外,还有 Kotlin Multiplatform 和 Compose Multiplatform 的诸多更新。上面视频涉及到的内容很多,感兴趣的很建议去观看,站内也有简单的摘录。今天这篇文章,我们来 KotlinConf 和它里面的一个主题演讲:Kotlin 新语法前瞻。
未来的新语法
对应视频位置:Keynote: www.bilibili.com/video/BV18D… | 主题演讲: www.youtube.com/watch?v=tAG…
Guards | 守卫
假设我们有一个 Status 类,它有三个子类:Loading、OK 和 Error。我们想要根据不同的状态来渲染不同的 UI。在之前,我们可能会这样写:
fun render(status: Status): String =
when {
status == Status.Loading -> "Loading"
status is Status.OK && status.data.isEmpty() -> "No data"
status is Status.OK -> status.data.joinToString()
status is Status.Error && status.isCritical -> "Critical problem"
else -> "Unknown problem"
}
这段代码工作正常,但我们却不得不写了很多 status 变量,一个理想的解决方案是把它放到 when 语句中,就像:
fun render(status: Status): String =
when (status) {
Status.Loading -> "Loading"
is Status.OK && status.data.isEmpty() -> "No data"
is Status.OK -> status.data.joinToString()
is Status.Error && status.isCritical -> "Critical problem"
else -> "Unknown problem"
}
但 && 位置会报错,Kotlin 编译器会提醒我们:Error, expecting a '->'。因为像这样使用 when 语句有一个严格的限制条件:每一个分支都仅能有单个判断。为此,Kotlin 将通过引入 Guard Condition(守卫条件)来消除这个限制,如下:
fun render(status: Status): String =
when (status) {
Status.Loading -> "Loading"
// 注意:下面的 && 变成了 if
is Status.OK if status.data.isEmpty() -> "No data"
is Status.OK -> status.data.joinToString()
is Status.Error if status.isCritical -> "Critical problem"
else -> "Unknown problem"
}
通过 if,我们可以额外加上其他判定条件。
该功能将在 Kotlin 2.1 中进入 Beta 状态
Context Sensitive Resolution | 上下文关联解析
继续我们上面的例子,你会发现仍然不够好:我们重复了很多个 Status.xxx。事实上,如果这个 Status 是一个密封类,我们其实已经知道判定条件都是 Status 的子类。为此,新语法进一步简化代码:
fun render(status: Status): String =
when (status) {
// 我们直接省略了前置的 Status. ,由编译器自动推断
Loading -> "Loading"
is OK if status.data.isEmpty() -> "No data"
is OK -> status.data.joinToString()
is Error if status.isCritical -> "Critical problem"
else -> "Unknown problem"
}
对于密闭类型和枚举,前置的冗余类型名称将可以省略了!
enum class Status {
Loading, OK, Error
}
fun process(
- status: Status = Status.Loading,
// 这里的 Status. 也可以省略
+ status: Status = Loading,
...
)
该功能将在 Kotlin 2.2 中进入 Experimental 状态
用双 $ 在字符串中引入变量
对于如下 JSON:
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://example.com/product.schema.json",
"$dynamicAnchor": "meta",
"title": "Product",
"type": "object"
}
有一些键是 $ 开头的。如果我们想把它以 Kotlin 的多行字符串形式表示出来,我们可能会写:
val json = """
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://example.com/product.schema.json",
"$dynamicAnchor": "meta",
"title": "Product",
"type": "object"
}
"""
但很可惜,这会报错,因为 Kotlin 会把 schema、id、dynamicAnchor 等当作变量。为了解决这个问题,你可能会想在 $ 前面加上一个 \,如下:
val json = """
{
"\$schema": "https://json-schema.org/draft/2020-12/schema",
"\$id": "https://example.com/product.schema.json",
"\$dynamicAnchor": "meta",
"title": "Product",
"type": "object"
}
"""
然而,这对多行字符串("""...""")无效。因为多行字符串里没有转义字符。要想解决,目前也有一些奇怪的方案,比如:
// 通过写成 ${'$'} 来间接表示 $
val json = """
{
${'$'}schema: "https://json-schema.org/draft/2020-12/schema",
${'$'}id: "https://example.com/product.schema.json",
${'$'}dynamicAnchor: "meta",
"title": "Product",
"type": "object"
}
"""
就……很奇怪。
在新语法中,我们可以在 """ 前面加上 $$,这样字符串将使用 $$ 引入变量,而不是 $:
// 通过在 """ 前面加上 $$,单个 $ 将成为普通字符
val json = $$"""
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://example.com/product.schema.json",
"$dynamicAnchor": "meta",
"title": "Product",
"type": "object"
}
"""
该功能将在 Kotlin 2.1 中进入 Beta 状态
Non-Local Continue/Break | 非局部 continue/break
for (i in 0..n) {
val data = state[i].let {
when (it) {
-1 -> break
0 -> continue
else -> process(it)
}
}
}
在这个例子中,我们循环 [0, n],对每一个 state[i] 进行处理。我们期望:如果 state[i] 为 -1,我们就退出循环;如果为 0,我们就跳过这一次循环;否则,我们就处理它。但这段代码会报错,因为当前 Kotlin 会认为这两个 break 和 continue 在 let 语句中,而不是在循环中。
但让我们看看 let 的定义:
inline fun <T, R> T.let(block: (T) -> R): R = block(this)
这是个内联函数,所以,其实上面的代码相当于:
for (i in 0..n) {
when (state[i]) {
-1 -> break
0 -> continue
else -> process(state[i])
}
}
实际上是合法的。新的 Kotlin 语法将允许此情况发生,上述代码将不再报错。
该功能将在 Kotlin 2.1 中进入 Beta 状态
Context Receivers Parameters | 上下文接收者参数
接下来是一个小的但是也很重要的语法变化。让我们来看个语法,它在过去 Kotlin 的发展中被自然的引入了——Context Parameters(或者也叫 Context Receivers)。假设我们有下面的代码:
withAutoClose {
// 我们假定这个 Lambda 作用域内,打开的文件能被自动关闭
val config = open(File("config.json"))
// 执行完后,文件会被自动关闭
}
为此,我们引入了一个特殊的 Scope 以及它的 open 拓展函数
interface AutoCloseScope
fun withAutoClose(block: AutoCloseScope.() -> Unit)
fun AutoCloseScope.open(file: File): InputStream
现在的问题是,如果我们希望把 open 作为 File 的拓展函数,使得整体的代码更加优雅、易读,我们可能会这样写:
- val config = open(File("config.json"))
+ val config = File("config.json").open()
但这是做不到的,因为 open 已经有上下文 AutoCloseScope 了。这个时候,新的 Context Parameters 就派上用场了。我们可以这样定义 withAutoClose:
- fun withAutoClose(block: AutoCloseScope.() -> Unit)
+ fun withAutoClose(block: context(_: AutoCloseScope).() -> Unit)
现在,就可以如下定义 open,它变成了 File 的拓展函数,且在 AutoCloseScope 的上下文中:
context(scope: AutoCloseScope)
fun File.open(): InputStream
该功能将在 Kotlin 2.2 中进入 Beta 状态
GADT-Style Smart-Cast | GADT 风格的智能转换
GADT(Generalized Algebraic Data Types)是一种函数式编程语言中的类型系统,它允许我们在类型中嵌入更多的信息。
此处使用 GADT 的叫法仅是为了便于理解,Kotlin 并没有这个术语,未来也可能变化
先给定以下代码:
sealed class Container<T>(val value: T) {
// 42? 宇宙的终极答案就是 42 啦
class IntContainer: Container<Int>(42)
// 这是本篇博客作者的名字
class StringContainer: Container<String>("FunnySaltyFish")
}
// 简单的拆箱
fun <V> unbox(container: Container<V>): V = container.value
但如果我们想进一步处理,比如说想写成下面这样子,就会报错:
fun <V> unboxAndProcess(container: Container<V>): V =
when (container) {
is Container.IntContainer -> 42
// Actual type: String, Expected type: V
is Container.StringContainer -> "FunnySaltyFish"
}
现阶段,我们只能强制转换并加上 @Suppress("UNCHECKED_CAST") 来处理这种情况,就像
@Suppress("UNCHECKED_CAST")
fun <V> unboxAndProcess(container: Container<V>): V =
when (container) {
is Container.IntContainer -> 42 as V
is Container.StringContainer -> "FunnySaltyFish" as V
}
但显然我们知道,在运行时完全不需要这个强转,是不会有问题的。而在 GADT 风格下,泛型 V 将会是 Int 或 String,取决于类型推断的结果。就像这样:
fun <V> unboxAndProcess(container: Container<V>): V =
when (container) {
is Container.IntContainer -> container.value // V 会被推断为 Int
is Container.StringContainer -> container.value // V 会被推断为 String
}
此项目仍在探索中,官方已经取得了可观的进展,预计将很快发布 KEEP(Kotlin Evolution and Enhancement Process)提案。
Name-based Destructuring | 基于名称的解构
对于现有的 data class,Kotlin 允许用任意名称解构它们(依赖于 componentN 函数),但这可能导致一些错误情况,比如:
data class Person(val name: String, val lastName: String)
fun process(person: Person) {
// 你可能会写成这样,但其实解构解错了
val (surname, firstName) = person
}
因此,在 Kotlin 2.x 的某一个版本,官方可能会为这种情况提供警告,最终将演化为报错:
data class Person(val name: String, val lastName: String)
fun process(person: Person) {
// Warning: "surname" doesn't match property "name"
// Warning: "firstName" doesn't match property "lastName"
val (surname, firstName) = person
}
到那时,使用任意名称解构 data class 将无法完成。最终官方将彻底禁止基于 componentN 函数的解构,data class 也是少生成一部分代码。官方也将提供新的方式用于解构 data class,因此未来使用 data class 的代码将更加安全。
此功能将在主要在 Kotlin 2.2 中完成 译者吐槽:我感觉这处理不好会很难受啊,比如说 Pair 这种,很少会有人使用
val (first, second) = pair吧,而是用更加富有语义的名称。看看未来的发展吧
Extensible Data Arguments | 函数参数整体传参
要引入这个语法,我们先来看一个背景:LazyColumn,这是 Jetpack Compose (确切地说,是 Compose Multiplatform)中的一个 Composable(UI 控件),用于显示惰性列表。使用起来大概如下:
LazyColumn {
// 显示 100 个 Item
items(100) { index ->
Text("Item #$index")
}
}
我们今天看看它的 API,作为一个 Composable,它有很多参数:
如果我们想加一个参数,又保持向后兼容性呢?我们就不得不创建一个新的 Overload,复制代码、复制 Doc。这就导致如果你点击看它的源码,能看到很多很多类似但只是一个参数不同的函数:
这样的问题在 Compose 的源码中比比皆是,各种文件一大部分就是为了保持向后兼容性的废弃代码。这样的代码维护起来是非常痛苦的。为此,Kotlin 计划引入了 dataarg,它允许我们把共性的参数提取出来,整体传入,就像这样:
// 提取出的共性参数,各种 LazyColumn/LazyGrid/Column/Row 都有,可以统一起来了
+ dataarg class ColumnSettings(
+ val contentPaddings: PaddingValues = PaddingValues(0.dp),
+ val reverseLayout: Boolean = false,
+ val verticalArrangement: Arrangement.Vertical = if (reverseLayout) Arrangement.Bottom else Arrangement.Top,
+ val horizontalAlignment: Alignment.Horizontal = Alignment.Start,
+ val userScrollEnabled: Boolean = true,
+)
// 以 LazyColumn 为例,我们可以这样传参
@Composable
fun LazyColumn(
modifier: Modifier = Modifier,
state: LazyListState = rememberLazyListState(),
+ dataarg args: ColumnSettings,
- contentPaddings: PaddingValues = PaddingValues(0.dp),
- reverseLayout: Boolean = false,
- verticalArrangement: Arrangement.Vertical = if (reverseLayout) Arrangement.Bottom else Arrangement.Top,
- horizontalAlignment: Alignment.Horizontal = Alignment.Start,
- userScrollEnabled: Boolean = true,
flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(),
content: LazyListScope.() -> Unit
) {
// ...
}
这大大节省了代码量,也让代码更加易读,同时保持良好的向后兼容性。
在调用处,我们仍然可以像以前一样传参:
LazyColumn(
modifier = Modifier.fillMaxSize(),
state = listState,
// 这个 reverseLayout 属于 dataarg 的一部分
reverseLayout = true,
) {
// ...
}
此功能目前只是提案(KT-8214),将在 Kotlin 2.2 进入 Experimental 状态 译者:真不错,写 Compose 的我非常需要这各功能
Union Types for Errors | 联合错误类型
让我们来设想一个函数:last,它返回一个序列中符合条件的最后一个对象,如果找不到,就报 NoSuchElementException。你可能想象的写法是这样:
fun <T> Sequence<T>.last(predicate: (T) -> Boolean): T {
var last: T? = null
for (element in this) {
if (predicate(element)) {
last = element
}
}
return last ?: throw NoSuchElementException("No element matching the predicate was found.")
}
看着很理想,但问题来了,如果条件是 { it == null } 呢?这样的写法明显不正确。为了实现能对判空的情况正确处理,我们必须加入一个 found 变量用来记录有没有找到:
fun <T> Sequence<T>.last(predicate: (T) -> Boolean): T {
var last: T? = null
var found = false
for (element in this) {
if (predicate(element)) {
last = element
found = true
}
}
if (!found) {
throw NoSuchElementException("No element matching the predicate was found.")
}
@Suppress("UNCHECKED_CAST")
return last as T
}
这代码既复杂,又引入了一个不必要的类型转换(as T),显然不优雅。那还有更好的主意吗?让我们试着引入一个全局变量 NotFound,用它来表示一个未找到的情况
private object NotFound
fun <T> Sequence<T>.last(predicate: (T) -> Boolean): T {
var result: Any? = NotFound
for (element in this) {
if (predicate(element)) {
result = element
}
}
if (result === NotFound) {
throw NoSuchElementException("No element matching the predicate was found.")
}
return result as T
}
但这也不够优雅,因为 result 的类型是 Any?,我们还是需要一个类型转换。为了补全这个类型系统,Kotlin 将引入 对 Errors 和 Exceptions 的联合类型,让我们可以这样写:
// 变成了 error object
private error object NotFound
fun <T> Sequence<T>.last(predicate: (T) -> Boolean): T {
var result: T | NotFound = NotFound
for (element in this) {
if (predicate(element)) {
result = element
}
}
if (result === NotFound) {
throw NoSuchElementException("No element matching the predicate was found.")
}
return result // 将会被智能转换为 T
}
但值得指出的是,这个联合类型专为 Errors 和 Exceptions 设计,用于区分成功和失败的情况。官方不打算引入更广泛的联合类型。联合类型会为类型检查器带来不确定性,新的运算符 ?. 和 !. 也将引入。
此特性仍处于研究阶段
Explicit Backing Fields | 显式后备字段
让我们看下面的代码,这在 MVVM、MVI 模式中经常经常出现
class ViewModel {
private val _city = MutableStateFlow("Beijing")
// 通过 city 属性暴露出去,使其只读
val city: StateFlow<String> = _city
// 而更新则通过 updateXxx 函数
fun updateCity(newCity: String) {
_city.value = newCity
}
}
这非常常见,官方说有超过 10w 个 GitHub 上的文件中有这种写法。这包括:
- MutableLiveData -> LiveData (Android)
- MutableStateFlow -> StateFlow (Kotlin Flow)
- MutableList -> List (Kotlin Collections)
- MutableState -> State (Compose)
- ...
显然这表示需要些新语法来简化这个过程。为此,Kotlin 将引入 field 关键字的新用法。上述的变量定义将简化为:
class ViewModel {
val city: StateFlow<String>
field = MutableStateFlow("Beijing")
}
这将自动生成 private 的 _city。当然,看到这个语法,你可能会问:
- 如果我在 public 的 inline 函数中使用,它的可访问性是 public 还是 private?
- 如果我从函数返回这个 field,它是什么呢?它应该是个 function 还是 getter?
- ...
现在官方的想法是,将引入可选的名称来解决这些问题,譬如:
class ViewModel {
val city: StateFlow<String>
field mutableCity = MutableStateFlow("Beijing")
val background: Color
field = mutableStateOf(getBackgroundColor)
get() = field.value
}
此特性将在 Kotlin 2.0 中 Experimental,2.2 中进一步变化
上述是未来即将到来的,除此之外,K2 作为一个全新的编译器,带来了很多能力上的优化,挑选部分列举如下:
Kotlin 2.0 已有的小优化
更强的 FIR
对于下列代码:
fun add(list: MutableList<Long>) {
list[0] = list[0] + 1
}
我们给 list[0] 加 1,在这里,Kotlin 正确处理了 Int 到 Long 的转换。但是,如果我们写成:
fun add(list: MutableList<Long>) {
list[0] += 1 // Error, 1L is required
}
在 Kotlin 1.x 中这会报错,因为它没有正确处理运算符合并(+ 和 =)加上类型转换。在 Kotlin 2.0 中,强大的新 FIR(Frontend Intermediate Representation)编译器将把其脱糖为类似下面的代码:
fun add(list: MutableList<Long>) {
list.set(0, list.get(0).plus(1))
}
在这个简单的例子中,我们事实上用了三种运算符([] - get、+ - plus、=)和一次类型转换。如果有可空运算符参与,这个例子还可以变得更加复杂:
class Box(val list: MutableList<Long>)
fun add(box: Box?) {
box?.list[0] += 1 // Error, 1L is required
box?.list[0] += 1L // 在 Kotlin 1.x 中,仍然会报错
}
在 Kotlin 2.0 中,这个例子也能正确编译,它会被脱糖为:
box?.run {
list.set(0, list.get(0).plus(1))
}
如各位所知,Kotlin 的强大来自它的可组合性(组合使用 运算符重载 、委托、拓展函数 等),而新的 FIR 能让其更为一致和健壮。
更强大的智能类型转换
K2 现在有了新的控制流引擎,帮助我们推断代码行为。在 Kotlin 中,这也表现为更加智能的类型转换。让我们来看一些例子:
变量参与到智能类型转换中
class Animal
class Cat: Animal {
fun meow() = println("Meow")
}
fun moew(animal: Animal) {
if (animal is Cat) {
animal.meow() // animal 被智能转换为 Cat,可以执行 meow
}
}
这个例子是个非常基础的类型转换,但如果我们稍微变一下,把 animal is Cat 提取出来,就不行了:
fun moew(animal: Animal) {
val isCat = animal is Cat
if (isCat) {
animal.meow() // 仅仅提取了个变量,就不行了。这一行在 Kotlin 1.x 中会报错
}
}
造成这的原因是,旧有代码中,仅有少量类型的表达式会对类型转换做贡献,比如类型检查(Type Checks)和可空性检查(Null Checks),而变量不会。自 K2 起,局部变量也能参与到类型转换中,携带必要的控制流信息,最终使得上述代码能够正确编译。
而说到可组合性,上述特性对所有智能转换都有效,甚至包括 Contracts。
Contract(契约)是一种 Kotlin 面向编译器约定的一种规则,比如 isNotEmpty 这种函数,就会用 Contract 告诉编译器传入的参数经此函数后如果返回 true,那么它就不为空列表。更多介绍可以参考:juejin.cn/post/705678…
class Card(val title: String?)
fun cardName(card: Any): String {
val cardWithNotNullTitle = card is Card && !card.title.isNullOrEmpty()
return when {
// 这里 cardWithNotNullTitle 贡献了两种规则:card 是 Card 类型,且 title 不为空,所以返回值直接判断应为 String
cardWithNotNullTitle -> card.title
else -> "无标题"
}
}
如上。
可变内联 Lambda 闭包中的智能转换
下一个例子发生在内联 lambda 内部:
fun indexOfMax(array: IntArray): Int? {
var maxIndex: Int? = null
array.forEachIndexed { index, value ->
// 在 Kotlin 1.x 中,即使 || 后面已经表示 maxIndex 非空,也无法省略这个 !! 的判断
// Smart cast to 'Int' is impossible,
// 'maxIndex' is a local variable that is captured by a changing closure
if (maxIndex == null || array[maxIndex!!] <= value) {
maxIndex = index
}
}
return maxIndex
}
在这个例子中,由于 maxIndex 定义在这个函数外,且在 lambda 内部使用,旧的 Kotlin 编译器会认为可能这个值会在别的地方被修改,无法推导出它非空。但事实上,我们知道,forEachIndexed 是一个内联函数,会在此处执行,且它无法被存储起来以修改。自 K2 起,Kotlin 编译器会认为所有的内联函数具有默认的 call in place(原地执行) Contract,能够正推断出 maxIndex 的非空性。
|| 运算符后的智能转换
假设我们有如下 Interface,它们组成了一个层级:
interface Status {
fun signal()
}
interface Ok: Status
interface Postponed: Status // Postpone:推迟
interface Decline: Status // Decline:拒绝
我们定义一个函数,当 Status 是 Postponed 或 Decline 时,调用 signal 通知:
fun process(object: Any) {
if (object is Postponed || object is Decline) {
// 很遗憾,这里会报错。因为 Kotlin 1.x 仍然会认为 object 是 Any 类型
object.signal()
}
}
在旧的版本中,编译器对于 || 运算符不会做智能转换。自 Kotlin 2.0.0 起,|| 将会尝试合并两个类型为其公共父类,上述的 object 将会被智能转换为 Status。
除了这些,Kt 2.0 还有大量诸如此类的优化,如下图所示:
此处不再详细列出,感兴趣的可以参考 Release Page
观众提问
主题演讲中有一些很有意思的观众提问,演讲者也给出了很有针对性的回答,此处也一并列出(下文中的“我”和“我们”均指的是演讲者和 Kotlin 团队,由于提问和回复表述比较繁琐,译者做了简化和合并):
问:是否会提供迁移工具,帮助迁移到新的语法?
是的,尤其是对于简单的语法。总的来说,我们会在 IDEA Plugin 或者 Fleet 中提供一些工具,帮助你迁移到新的语法
问:为什么 Guards 选择了
if而不是原先的逻辑运算符,比如 && 呢?那还能允许 || 这种。
事实上,最初我们确实想用 &&,那很直白,你完全不需要学新东西。但我们发现了一些问题,比如当你真的需要逻辑运算符的时候,整体有些混乱。因为这个 && 其实不是那个“逻辑和”,它在前面的条件成立时才执行,那可能会导致你认为的某些为 true 的条件实际上是 false。这也是为啥我们选择了明显的 if 以作区分
问:我们知道,Java 学习了很多 Kotlin 的特性,也在飞速发展;那 Kotlin 会不会从 Java 中带来一些东西呢?
好问题,大概来说,目前 Java 比 Kotlin 强的地方可能是它的模式匹配,它们引入了基于位置的模式匹配(译注:可参考此处),在 Kotlin 可能有一些前向兼容问题,但仍然非常强大。我们有点犹豫是否要为 Kotlin 也引入类似语法,我们正在观察 Java 的动向,如果开发者们普遍接受了这种基于命名的解构,我们可能会考虑引入去除基于位置解构这一部分的模式匹配。
问:我还想回到那个 guard 的示例上
fun render(status: Status): String = when (status) { Status.Loading -> "Loading" // 注意:下面的 && 变成了 if is Status.OK if status.data.isEmpty() -> "No data" is Status.OK -> status.data.joinToString() is Status.Error if status.isCritical -> "Critical problem" else -> "Unknown problem" }这里你省略了前面的
status,但是后面的status.data之类的却还是要保留status.,为什么不做一个隐式的this指向这个status呢,这样还可以进一步简化。
感谢,事实上,我们确实讨论过,我们甚至想过用 $ 这种来指代。但我们尝试了后发现这并不能带来很大帮助,而且有时候让代码太过晦涩了。因为有时候你需要用这个变量,有时候不需要。
问:为什么不考虑把联合类型推广到普遍情况呢,而不仅仅是限定于错误类型?您提到了一些问题是对于错误实现联合类型所可能面对的,既然都要面对,为什么不考虑一起做了呢?
好的,关键问题其实在于,我们希望创造一种独立的类型,用于表示错误,对于这种类型有可能实现联合类型。这种类型有一些方法比如 throw,因此可以与 Exception 兼容,并最终让所有的 Exception 和 Error 构建于其之上。至于推广,我们事实上有一个多项式复杂度的类型判定算法,这基于你有一个确定的主类型,以及一系列其他类型。我们得确保这个主类型是唯一的确定的。
问:当工作于一个大型的多模块应用时,我们有时发现跨模块的智能转换无法工作,而且有些库,比如提供不可变类型的,在智能转换时也不工作,有什么这方面的工作吗?
额,实话说,短期内我们确实没有跨模块的智能转换方面的计划。不过我们也确实需要更多的关于不可变性的特性,在未来确实有可能的
问:如果真的取消了不同名的解构,那如果我们需要,比如说解构两个同类的对象,怎么办呢?
对对对,非常有道理。我们会提供一些特殊语法来帮助重命名属性,就像调用函数时的命名参数一样,会有类似的东西的
问:对于新的显示后备字段,有办法让其参与到序列化中码,比如 Kotlinx.serialization?
是的,这也是我们让这些新语法与现有工具适配的一小部分,因为这些序列化工具,比如 Jackson,非常流行。简单地说,可以的。
问:对于联合错误类型,有办法做到像 Java 那样的多异常捕获吗?
是的,这也是这一特性力图解决的问题之一(现场鼓掌)
问:对于新的后备字段,有办法提供多个 Setters,为不同的传入参数类型提供不同的重载吗?
啊,这是关于更新不可变数据的一部分,是的,我们有类似的设计,你可以参考我门的 KEEP 仓库。有一个由 Arrow(?)提供的编译器插件,提供了类似 Java 的语法,可以开一个 Lambda 去做更新,但我们觉得这不是最佳方式。因为,比如说你想更新一个嵌套很深的值,你就需要写很多嵌套的 Lambda,这有点疯狂;同时这也让编译器很难只更新有更改的部分。
另外除了他重点演示的这些语法,还有一些别的小语法只给了名称但没有细说,此处也不再列举了。
社区讨论
既然谈到了语法,那不妨也聊一聊社区的声音。Kotlin的官方论坛 在这里也有很多想法,我们挑选两个上文被提到的话题,来看看社区的讨论。
以下内容均为社区讨论,不代表 JetBrains 官方立场。下文提到的 “现在” 均指 2024-05-28
Union Types | 联合类型
关于联合类型的讨论几乎是最热门的话题,而且持续时间也非常久。这个话题 从 2015 年 10 月开启,到现在有 127 条回复,10w+ 的浏览。当然,Kotlin 自己说不太可能引入更广泛的联合类型了,但是讨论说不定还是可以看看的。
大多数人希望参考类似 TS、Python 的联合类型,能设计出类似如下的代码:
fun foo(x: Int | String) {
when (x) {
is Int -> println(x + 1)
// smart cast as String
else -> println(x.length)
}
}
译者本人确实非常期待这个特性,对于 Jetpack Compose 这种 UI 框架,能大幅度减少代码量。现有的各种控件为了处理不同类型的输入,必须要写一大堆函数,比如 Text:
@Composable
fun Text(
text: String, // 接收 String 类型
modifier: Modifier = Modifier,
color: Color = ...,
fontSize: TextUnit = ...,
fontWeight: FontWeight = ...,
fontStyle: FontStyle = ...,
letterSpacing: TextUnit = ...,
...
)
@Composable
fun Text(
text: AnnotatedString, // 接收 AnnotatedString 类型
modifier: Modifier = Modifier,
color: Color = ...,
fontSize: TextUnit = ...,
fontWeight: FontWeight = ...,
fontStyle: FontStyle = ...,
letterSpacing: TextUnit = ...,
...
)
如果有联合类型,我们就可以把这两个函数合并成一个:
@Composable
fun Text(
text: String | AnnotatedString, // 接收 String 或 AnnotatedString 类型
modifier: Modifier = Modifier,
color: Color = ...,
fontSize: TextUnit = ...,
fontWeight: FontWeight = ...,
fontStyle: FontStyle = ...,
letterSpacing: TextUnit = ...,
...
)
整体代码就会简洁非常多(比 dataarg 还简洁)。这种情况在 Icon 这种也很明显,如果想混用 ImageVector 和 Painter,就必须写两个函数,对于封装的基础控件就得一层层往上套,如果有联合类型就好非常多。而且,同为 JVM 语言的 Scala 也有联合类型,这也证明了编译角度的可行性。
评论区的大家提出了各种用法的畅享,比如用于 typealias
typealias FlexibleDate = String | Long | Instant
// 接收不同的时间类型
fun bar( x : String | Long) { ... }
fun foo( y : FlexibleDate) {
if (!(x is Instant)) { // so x is String | Long
bar(x)
}
}
或者归类一些错误:
object NoInternetConnectionException
object NoSignedInUserException
data class InternalException(val origin: Throwable)
data class UnknownException(val origin: Throwable)
typealias AddException =
NoInternetConnectionException | NoSignedInUserException | UnknownException
typealias FindException =
NoInternetConnectionException | NoSignedInUserException | InternalException | UnknownException
typealias FindAllException =
NoInternetConnectionException | NoSignedInUserException | InternalException | UnknownException
甚至有人设计了在此基础上的运算符:
T | A— 类型 A 或 TT & A— 类型 A 和 TT & ~A— 类型 T 但不是 A~A— 不是 A 的任何类型
且有:
- 若
T = A | B | C,那么T & ~A = B | C
基本上就像二进制运算符,但是用于类型。
也有人给出了 when 语句中的使用,这样可以按层级的提取公共项,然后聚合相同的代码:
return when(union) {
is A,
is B -> when(union) {
// smart-cast `union` to A|B union type
// do something common for A & B
when(union) {
is A -> // do A-specific
is B -> // do B-specific
// don't handle `is C`
}
}
is C -> // do C-specific
}
评论区的小伙伴儿也讨论了很多可能的编译实现,回复里也有官方以前的回复。唉,不过现在只能看着欣赏了。
Pattern Matching | 模式匹配
另一个热门话题是模式匹配,在问答环节也提到过。这个话题 从 2017 年 5 月开启,到现在有 28 条回复,5.7w+ 的浏览。
模式匹配是一种非常强大的特性,譬如在 Rust、Scala、Python 等语言中都有。它可以让我们在分支语句中直接解构获取对象的一部分。比如如下代码:
// 数据类,有 componentN 函数
data class Person(val name: String, val age: Int)
when(person) {
Person("sam", age) -> println("这个匹配所有名为 sam 的人,并将 $age 作为参数传递")
Person(name, _) -> println("这个匹配所有人,并将 $name 作为参数传递,忽略年龄")
}
在另一个 JVM 系语言 Scala 中,如果使用字面量,那么它将匹配该值,如果使用变量名,它将提取该值并将其分配给变量,如果在反引号中使用变量名,则它将匹配已定义变量的值(但该变量必须是“常量”)。
另外的经典例子出现在 Pair 和 Triple 上:
when (pair) {
("不及格", _) -> println("这个匹配所有第一个元素为不及格的 Pair")
("优秀", score) -> println("这个匹配所有第一个元素为优秀的 Pair,并将 $score 作为参数传递")
(status, score) -> println("这个匹配所有 Pair,并将 $status 和 $score 作为参数传递")
}
when (obj) {
(true, "OK", _) -> println("这个匹配所有第一个元素为 true,第二个元素为 OK 的 Triple")
(false, "OK", data) -> println("这个匹配所有第一个元素为 false,第二个元素为 OK 的 Triple,并将 $data 作为参数传递")
(false, status, data) -> println("这个匹配所有 Triple,并将 $status 和 $data 作为参数传递")
}
除了用在 when 语句,还有人发挥想象,提出了放在函数里:
fun foo(1) = 1 // 直接让 foo 传入参数为 1 时返回 1
fun foo(2) = 1
fun foo(n: Int) = foo(n-1) + foo(n-2)
/**
计算斐波那契数列
等价于
fun foo(n: Int) = when(n) {
1, 2 -> 1
else -> foo(n-1) + foo(n-2)
}
*/
感觉蛮神奇的。
不过评论区有提到,JetBrains 官方可能认为这个特性太复杂,但ta也没有给具体链接。反正嘛,放在这权当畅想了,说了去掉基于位置的解构,也不知道真出来得是什么样子了。
以上就是本文全部内容。除新语法外,Kt 2.0.0 以及 Compose Multiplatform 1.6.10 的发布也有很多亮点(比如两倍的编译速度,CMP 对 iOS 进入 Beta、WasmJs 进入 Alpha 等)。作为一个用 Jetpack Compose 两年半、CMP 半年的开发者,很期待看到它进一步成熟。此次 Keynote 中,Kotlin Project Lead 表示,Kt 2.0.0 经过了 JetBrains 上千万行的代码验证,非常稳定,也有公司(如 Meta)已经大规模迁移。这也是我在文章开头提到的,很建议去观看原视频。所以说,xdm,你们会尝试用起来吗?