Kotlin 1.7.0 正式发布!主要新特性一览

6,963 阅读11分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第3天,点击查看活动详情


6月9号,Kotlin 发布了1.7.0正式版。本文将大致过一遍主要的新特性。

本文主要来自 What's new in Kotlin 1.7.0 | Kotlin,部分来自 Kotlin 1.7.0-Beta 现已发布 | The Kotlin Blog ,完整内容也请参考这些链接。受限于本人水平,难免有误,敬请谅解。

作者:FunnySaltyFish (github.com)

以下是此版本主要更新:

新的 Kotlin K2 编译器

此 Kotlin 版本引入了新的 Kotlin K2 编译器的 Alpha 版本。新的编译器旨在加快新语言功能的开发,统一Kotlin支持的所有平台,带来性能改进,并为编译器扩展提供API。

我们已经发布了一些关于我们的新编译器及其优点的详细说明:

需要强调的是,对于新K2编译器的Alpha版本,我们主要关注性能改进,并且它仅适用于JVM项目。它不支持Kotlin / JS,Kotlin / Native 或其他多平台项目,并且包括 kapt 在内的编译器插件都无法使用它。

我们的基准测试显示了我们内部项目的一些杰出成果:

Project现版本 Kotlin 编译器新的 K2 Kotlin 编译器相对提升
Kotlin2.2 KLOC/s4.8 KLOC/s~ x2.2
YouTrack1.8 KLOC/s4.2 KLOC/s~ x2.3
IntelliJ IDEA1.8 KLOC/s3.9 KLOC/s~ x2.2
Space1.2 KLOC/s2.8 KLOC/s~ x2.3

KLOC/s 为每秒钟编译器处理的源代码行数(千行)

您可以查看 JVM 项目的性能提升,并将其与旧编译器的结果进行比较。要启用 Kotlin K2 编译器,请使用以下编译器选项:

-Xuse-k2

此外,K2 编译器 [还包括许多错误修复](youtrack.jetbrains.com/issues/KT?q…: fixed-in-frontend-ir sort by: Priority, votes, updated)。请注意,此列表中以 State: Open 开头的 bugs 实际上在 K2 里也已得到修复。

下一个 Kotlin 版本将提高 K2 编译器的稳定性并提供更多功能,敬请期待并提供您的反馈!

语法

内联类的内联值也能委托了

如果要为值或类实例创建轻量级wrapper,则必须手动实现所有接口方法,委托实现解决了这个问题。但在 1.7.0 之前,它不适用于内联类。此限制已被删除,因此您现在可以创建在大多数情况下不分配内存的轻量级wrapper。

// 接口,唯一的方法返回一个字符串
interface Bar {
    fun foo() = "foo"
}

@JvmInline
// kt1.7之前,对内联类不能如此写
// 否则会报 "Value class cannot implement an interface by delegation if expression is not a parameter"
// kt1.7 解除了这个限制
value class BarWrapper(val bar: Bar): Bar by bar

fun main() {
    val bw = BarWrapper(object: Bar {})
    println(bw.foo())
}

类型参数的下划线运算符

Kotlin 1.7.0 为类型参数引入了一个下划线运算符 。可以使用它在指定其他类型时自动推断类型参数:_

abstract class SomeClass<T> {
    abstract fun execute(): T
}

class SomeImplementation : SomeClass<String>() {
    override fun execute(): String = "Test"
}

class OtherImplementation : SomeClass<Int>() {
    override fun execute(): Int = 42
}

object Runner {
    inline fun <reified S: SomeClass<T>, T> run(): T {
        return S::class.java.getDeclaredConstructor().newInstance().execute()
    }
}

fun main() {
    // T 被推断为 String,因为 SomeImplementation 继承自 SomeClass<String>
    val s = Runner.run<SomeImplementation, _>()
    assert(s == "Test")

    // T 被推断为 Int ,因为 OtherImplementation 继承自 SomeClass<Int>
    val n = Runner.run<OtherImplementation, _>()
    assert(n == 42)
}

构建器推断变更

构建器推断 (Builder inference) 是一种特殊的类型推断,在调用泛型构建器函数时很有帮助。 它可以帮助编译器推断调用的类型实参,方法是使用其 lambda 实参中其他调用的相关类型信息。

在此版本中,如果常规类型推断无法获得有关类型的足够信息,即可自动激活构建器推断。(以前需额外指定 -Xenable-builder-inference 编译器选项——在 1.6.0 版本中引入)。这意味着现在您无需应用任何额外的注解或选项,即可编写自己的使用构建器类型推断的构建器。

FunnySaltyFish:放几个栗子:

val result = buildList {
    // Type argument is inferred into Float based on the expected type
    val x: Float = get(0)
} // result has the List<Float> type
fun takeMyLong(x: Long) { ... }

fun String.isMoreThat3() = length > 3

fun takeListOfStrings(x: List<String>) { ... }

fun main() {
    val result1 = buildList {
        val x = get(0)
        takeMyLong(x)
    } // result1 has the List<Long> type

    val result2 = buildList {
        val x = get(0)
        val isLong = x.isMoreThat3()
    // ...
    } // result2 has the List<String> type

    val result3 = buildList {
        takeListOfStrings(this)
    } // result3 has the List<String> type
}

更多内容详见: 了解如何编写自定义通用构建器

稳定的 opt-in requirements

Opt-in requirements 已为 Stable ,无需添加额外的编译器参数.

在 1.7.0 之前, opt-in 需要指定参数 -opt-in=kotlin.RequiresOptIn 以避免 warning,现在不需要了; 不过,您仍然可使用 -opt-in 选择加入其他 annotations、 module-wise.

稳定的明确非空类型

在 Kotlin 1.7.0 中,绝对不可为 null 的类型已提升为 Stable。它们在扩展通用 Java 类和接口时提供了更好的互操作性。

使用新语法 T & Any 标记此为明确非空(绝对不可空)类型。 此语法来自 intersection types 的符号。在 & 左侧为可空的类型参数,右侧为不可空的Any

fun <T> elvisLike(x: T, y: T & Any): T & Any = x ?: y

fun main() {
    // OK
    elvisLike<String>("", "").length
    // 错误: 'null' 无法用于 non-null 值
    elvisLike<String>("", null).length

    // OK
    elvisLike<String?>(null, "").length
    // 错误: 'null' 无法用于 non-null 值
    elvisLike<String?>(null, null).length
}

在此 KEEP 中了解有关绝对不可为 null 的类型的更多信息。

标准库

在 Kotlin 1.7.0 中,标准库进行了一系列更改和改进。它们引入了新功能,稳定了实验性功能,并统一了对 Native、JS 和 JVM 的命名捕获组的支持:

min() 和 max() 集合函数回归

Kotlin 1.4 中,我们将 min()max() 集合函数重命名为 minOrNull()maxOrNull()。 这些新的名称能够更好地反映它们的行为 – 如果接收器集合为空,则返回 null。 它还有助于使函数的行为与整个 Kotlin Collections API 中使用的命名惯例保持一致。

minBy()maxBy()minWith()maxWith() 同样如此,在 Kotlin 1.4 中均具有自己的 *OrNull() 同义词。 受此变更影响的旧函数已逐渐弃用。

Kotlin 1.7.0-Beta 重新引入了原始的函数名称,但加入了不可空返回类型。 现在,更新后的 min()max()minBy()maxBy()minWith()maxWith() 会严格返回集合元素或抛出异常。

fun main() {
    val numbers = listOf<int>()
    println(numbers.maxOrNull()) // "null"
    println(numbers.max()) // "Exception in… Collection is empty."
}

正则表达式特定位置匹配

在 1.5.30 中引入Regex.matchAt()Regex.matchesAt() 函数现已达到稳定版本。 它们提供了一种方式来检查正则表达式在 StringCharSequence 中的特定位置是否具有精确匹配。

  • matchesAt() 可以检查匹配并返回布尔结果:
fun main(){
    val releaseText = "Kotlin 1.7.0 is on its way!"
    // 正则表达式: 一个数字, “.”, 一个数字, “.”, 一个或多个数字
    val versionRegex = "\\d[.]\\d[.]\\d+".toRegex()

    println(versionRegex.matchesAt(releaseText, 0)) // "false"
    println(versionRegex.matchesAt(releaseText, 7)) // "true"
}
  • matchAt() 会在找到匹配的情况下返回匹配,在未找到匹配的情况下返回 null
fun main(){
    val releaseText = "Kotlin 1.7.0 is on its way!"
    val versionRegex = "\\d[.]\\d[.]\\d+".toRegex()

    println(versionRegex.matchAt(releaseText, 0)) // "null"
    println(versionRegex.matchAt(releaseText, 7)?.value) // "1.7.0"
}

对以前语言和 API 版本的扩展支持

为了支持库作者 开发可在各种旧 Kotlin 版本中使用的库,并解决 Kotlin 主版本更新频率增加的问题,我们扩展了对以前语言和 API 版本的支持。

在 Kotlin 1.7.0 中,我们支持三个以前的语言和 API 版本,而不是两个。这意味着 Kotlin 1.7.0 支持针对 1.4.0 的 Kotlin 版本的库开发。有关向后兼容性的详细信息,请参阅兼容性模式

通过反射获取注解

1.6.0 首次引入的 拓展函数 KAnnotatedElement.findAnnotations() 现已进入 Stable. 此 反射 函数返回某元素特定类型的所有注解, 包括 独立使用 和 重复使用 的注解.

@Repeatable
annotation class Tag(val name: String)

@Tag("First Tag")
@Tag("Second Tag")
fun taggedFunction() {
    println("I'm a tagged function!")
}

fun main() {
    val x = ::taggedFunction
    val foo = x as KAnnotatedElement
    println(foo.findAnnotations<Tag>())
    // [@Tag(name=First Tag), @Tag(name=Second Tag)]
}

稳定的深度递归函数

深度递归函数 (DeepRecursiveFunction) 自 Kotlin 1.4.0 以来一直作为实验性功能提供,现在它们在 Kotlin 1.7.0 中是稳定的。使用DeepRecursiveFunction 可以定义一个函数,该函数将其堆栈保留在堆上,而不是实际的调用堆栈。这允许您运行非常深的递归计算。使用invoke以调用这类函数。

在此示例中,深度递归函数用于以递归方式计算二叉树的深度。即使此示例函数以递归方式调用自身 100,000 次,也不会抛出 StackOverflowError

class Tree(val left: Tree?, val right: Tree?)

val calculateDepth = DeepRecursiveFunction<Tree?, Int> { t ->
    if (t == null) 0 else maxOf(
        callRecursive(t.left),
        callRecursive(t.right)
    ) + 1
}

fun main() {
    // 生成一颗深度为 100000 的二叉树
    val deepTree = generateSequence(Tree(null, null)) { prev ->
        Tree(prev, null)
    }.take(100_000).last()

    println(calculateDepth(deepTree)) // 100000
}

若递归深度超过1000, 请考虑在代码中使用深度递归函数。

基于默认时间源的时间标记现在基于内联类

Kotlin 1.7.0 通过将 返回的时间标记更改为内联类,提高了时间测量功能的性能。这意味着调用像TimeSource.MonotonicmarkNow()elapsedNow()measureTime()measureTimedValue()TimeMark 这样的函数不会为其实例分配包装类。特别是在测量作为hot path一部分的代码段时,这有助于最大限度地减少测量对性能的影响:

@OptIn(ExperimentalTime::class)
fun main() {
    val mark = TimeSource.Monotonic.markNow() // 返回的 `TimeMark` 为内联类
    val elapsedDuration = mark.elapsedNow()
}

仅当从 TimeMark 中获取的时间源对 TimeSource.Monotonic 为静态时,此优化才可用。

Java Optionals 的新实验性扩展函数

Kotlin 1.7.0 附带了新的便利函数,简化了 Java 中Optional类的使用。这些新功能可用于在 JVM 上拆箱和转换可选对象,并帮助使 Java API 的使用更加简洁。

拓展函数getOrNull()getOrDefault()getOrElse() 允许您获取 Optional 的值(如果有的话)。否则,将视情况获得null、默认值或函数返回的值:

val presentOptional = Optional.of("FunnySaltyFish")

println(presentOptional.getOrNull())
// "FunnySaltyFish"

val absentOptional = Optional.empty<String>()

println(absentOptional.getOrNull()) // null
println(absentOptional.getOrDefault("给个默认值")) // "给个默认值"
println(absentOptional.getOrElse {
    println("Optional 值缺失")
    "默认值"
})
// "Optional 值缺失"
// "默认值"

扩展函数 toList()toSet()asSequence()将现有 Optional的值转换为列表、集合或序列,否则返回空集合。扩展函数 toCollection()将值追加到已存在的目标集合:

val presentOptional = Optional.of("I'm here!")
val absentOptional = Optional.empty<String>()
println(presentOptional.toList() + "," + absentOptional.toList())
// ["I'm here!"], []
println(presentOptional.toSet() + "," + absentOptional.toSet())
// ["I'm here!"], []
val myCollection = mutableListOf<String>()
absentOptional.toCollection(myCollection)
println(myCollection)
// []
presentOptional.toCollection(myCollection)
println(myCollection)
// ["I'm here!"]
val list = listOf(presentOptional, absentOptional).flatMap { it.asSequence() }
println(list)
// ["I'm here!"]

这些扩展函数在 Kotlin 1.7.0 中作为实验性引入。您可以在此 KEEP 中了解有关Optional扩展的更多信息。与往常一样,我们欢迎您在 Kotlin 问题跟踪器中提供反馈。

支持 JS 和本机中的命名捕获组

从 Kotlin 1.7.0 开始,命名捕获组不仅在 JVM 上受支持,在 JS 和 Native 平台上也受支持。

若要为捕获组命名,请在正则表达式中使用 (?<name>group) 语法。若要获取与组匹配的文本,请调用新引入的 MatchGroupCollection.get() 函数并传递组名。

按名称检索匹配的组值

fun dateReplace() {
    val dateRegex = Regex("(?<dd>\\d{2})-(?<mm>\\d{2})-(?<yyyy>\\d{4})")
    val input = "Date of birth: 27-04-2022"
    println(dateRegex.replace(input, "\${yyyy}-\${mm}-\${dd}")) // "Date of birth: 2022-04-27" — by name
    println(dateRegex.replace(input, "\$3-\$2-\$1")) // "Date of birth: 2022-04-27" — by number
}

命名反向引用

现在,您还可以在反向引用组时使用组名。反向引用与捕获组先前匹配的相同文本匹配。为此,请使用正则表达式中的语法:\k<name>

fun backRef() {
    val regex = "(?<title>\\w+), yes \\k<title>".toRegex()
    val match = regex.find("Do you copy? Sir, yes Sir!")!!
    println(match.value) // "Sir, yes Sir"
    println(match.groups["title"]?.value) // "Sir"
}

换表达式中的命名组

命名组引用可与替换表达式一起使用。请考虑 replace() 函数,该函数将输入中指定正则表达式的所有匹配项替换为替换表达式;以及仅替换第一个匹配项的 replaceFirst() 函数。

替换字符串中出现的 ${name} 将替换为与具有指定名称的捕获组相对应的子序列。您可以按名称和索引比较组引用中的替换项:

fun dateReplace() {
    val dateRegex = Regex("(?<dd>\\d{2})-(?<mm>\\d{2})-(?<yyyy>\\d{4})")
    val input = "Date of birth: 27-04-2022"
    println(dateRegex.replace(input, "\${yyyy}-\${mm}-\${dd}")) // "Date of birth: 2022-04-27" — by name
    println(dateRegex.replace(input, "\$3-\$2-\$1")) // "Date of birth: 2022-04-27" — by number
}

Gradle

好多,不翻了。见 kotlinlang.org/docs/whatsn…

除上述之外,还有一些涉及到 JS/Native 的部分没有翻译,感兴趣的可自行参阅原链接。

溜了。