Kotlin 1.4 新特性预览

Kotlin 1.4 没有特别重大的更新,更多的是细节的优化。html

1. 安装 Kotlin 1.4

Kotlin 1.4 的第一个里程碑版本发布了,具体发布信息能够在这里查看git

生产环境当中最好仍然使用 Kotlin 的稳定版本(例如最新的 1.3.71),若是你想要马上立刻体验 1.4 的新特性,那么个人建议是先安装一个 EAP 版本的 IntelliJ IDEA EAP 版本是 IntelliJ IDEA 2020.1 Beta,而后再在这个版本的 IntelliJ 上安装最新版的 Kotlin 插件,这样既能够继续使用 1.3 作项目,又不耽误体验新特性:github

图 1:IntelliJ IDEA EAP 版本与正式版能够共存

安装 Kotlin 1.4 的插件方法想必你们都已经轻车熟路了,打开设置,搜 Kotlin,找到插件版本管理的下拉菜单,选择 Early Access Preview 1.4.x 便可:面试

图 2:升级 Kotlin 插件

图 2:升级 Kotlin 插件

好了,重启 IntelliJ,新建一个工程试试看吧~~算法

2. 主要的语法更新

接下来咱们就按照官方博客给出的介绍 Kotlin 1.4-M1 Released 来体验下新特性。数组

本文源码均已整理至 GitHub:Kotlin1.4FeaturesSample并发

2.1 Kotlin 接口和函数的 SAM 转换

一个就是你们期待已久的 Kotlin 接口和函数的 SAM 转换。得益于新的类型推导算法,以前一直只有调用接收 Java 单一方法接口的 Java 的方法时才能够有 SAM 转换,如今这个问题不存在了,且看例子:app

//注意 fun interface 是新特性
    fun interface Action {
        fun run()
    }
    
    // Kotlin 函数,参数为 Kotlin 单一方法接口
    fun runAction(a: Action) = a.run()
    // Kotlin 函数,参数为 Java 单一方法接口
    fun runRunnable(r: Runnable) = r.run()
复制代码

在 1.4 之前,咱们只能:ide

runAction(object: Action{
        override fun run() {
            println("Not good..")
        }
    })
复制代码

或者函数

runAction(Action { println("Not good..") })
复制代码

runRunnable 函数虽然接收的是 Java 的接口,一样不支持 SAM。

如今在 1.4 当中呢?

runAction { println("Hello, Kotlin 1.4!") }
    runRunnable { println("Hello, Kotlin 1.4!") }
复制代码

真是妙啊。

2.2 类型推导支持了更多的场景

类型推导让 Kotlin 的语法得到了极大的简洁性。不过,你们在使用 Kotlin 开发时,必定会发现有些状况下明明类型是很肯定的,编译器却必定要让咱们显式的声明出来,这其实就是类型推导算法没有覆盖到的场景了。

例如如下代码在 Kotlin 1.3 当中会提示类型不匹配的问题:

val rulesMap: Map<String, (String?) -> Boolean> = mapOf(
        "weak" to { it != null },
        "medium" to { !it.isNullOrBlank() },
        "strong" to { it != null && "^[a-zA-Z0-9]+$".toRegex().matches(it) }
    )
复制代码

图 3:Kotlin 1.3 中提示类型不匹配

博客原文中给出的这个例子乍一看挺复杂,仔细想一想问题主要在于咱们能够经过 rulesMap 的类型来肯定 mapOf 的返回值类型,进而再肯定出 mapOf 的参数类型,即 Pair 的泛型参数类型。类型信息是充分的,不过这段代码在 Kotlin 1.4 之前是没法经过编译的,应该是类型推导的层次有点儿多致使算法没有覆盖到。好在新的推导算法解决了这个问题,可以应付更加复杂的推导场景。

2.3 Lambda 表达式最后一行的智能类型转换

这个比较容易理解,直接看例子:

val result = run {
       var str = currentValue()
        if (str == null) {
            str = "test"
        }
        str // the Kotlin compiler knows that str is not null here
    }
    // The type of 'result' is String? in Kotlin 1.3 and String in Kotlin 1.4```
复制代码

这里 result 做为 run 的返回值,实际上也是 run 的参数 Lambda 的返回值,所以它的类型须要经过 str 的类型来推断。

在 1.3 当中,str 的类型是能够推断成 String 的,由于 str 是个局部变量,对它的修改是可控的。问题在于虽然 str 被推断为 String 类型,Lambda 表达式的返回值类型却没有使用推断的类型 String 来判断,而是选择使用了 str 的声明类型 String?。

在 1.4 解决了这个问题,既然 str 能够被推断为 String,那么 Lambda 表达式的结果天然就是 String 了。

稍微提一下,IntelliJ 的类型提示貌似有 bug,有些状况下会出现不一致的状况:

图 4:疑似 IntelliJ 行内的类型提示的 bug

咱们能够经过快捷键查看 result 的类型为 String,可是行内的类型提示却为 String?,不过这个不影响程序的运行。

固然,有些开发者常常会抱怨相似下面的这种状况:

var x: String? = null
    
    fun main() {
        x = "Hello"
        if(x != null){
            println(x.length) 
        }
    }
复制代码

我明明已经判断了 x 不为空,为何却不能自动推导成 String?请必定要注意,这种状况不是类型推导算法的问题,而是 x 的类型确实没法推导,由于对于一个共享的可变变量来说,任何前一秒的判断都没法做为后一秒的依据。

2.4 带有默认参数的函数的类型支持

若是一个函数有默认参数,咱们在调用它的时候就能够不传入这个参数了,例如:

fun foo(i: Int = 0): String = "$i!"
复制代码

调用的时候既能够是 foo() 也能够是 foo(5),看上去就如同两个函数同样。在 1.4 之前,若是咱们想要获取它的引用,就只能获取到 (Int) -> String 这样的类型,显得不是很方便,如今这个问题解决了:

fun apply1(func: () -> String): String = func()
    fun apply2(func: (Int) -> String): String = func(42)
    
    fun main() {
        println(apply1(::foo))
        println(apply2(::foo))
    }
复制代码

不过请注意,一般状况下 ::foo 的类型始终为 (Int) -> String,除了做为参数传递给接收 () -> String 的状况下编译器会自动帮忙转换之外,其余状况下是不能够的。

2.5 属性代理的类型推导

在推断代理表达式的类型时,以往不会考虑属性代理的类型,所以咱们常常须要在代理表达式中显式的声明泛型参数,下面的例子就是这样:

import kotlin.properties.Delegates
    
    fun main() {
        var prop: String? by Delegates.observable(null) { p, old, new ->
            println("$old$new")
        }
        prop = "abc"
        prop = "xyz"
    }
复制代码

这个例子在 1.4 中能够运行,但若是是在 1.3 当中,就须要明确泛型类型:

var prop: String? by Delegates.observable<String?>(null) { p, old, new ->
        println("$old$new")
    }
复制代码

2.6 混合位置参数和具名参数

位置参数就是按位置传入的参数,Java 当中只有位置参数,是你们最熟悉的写法。Kotlin 支持了具名参数,那么入参时两者混合使用会怎样呢?

图 5:1.3 当中不容许在具名参数以后添加位置参数

1.3 当中,第三个参数会提示错误,理由就是位置参数前面已经有了具名参数了,这是禁止的。这样主要的目的也是但愿开发者可以避免写出混乱的入参例子,不过这个例子彷佛并不会有什么使人疑惑的地方,因而 1.4 咱们能够在具名参数后面跟位置参数啦。

其实这个特性并不会对入参有很大的影响。首先位置参数的位置仍然必须是对应的,其次具名参数的位置也不能乱来。例如咱们为例子中的 a 添加一个默认值:

图 6:1.4 当中具名参数以后添加位置参数须要保证位置对应

注意图 6 是 1.4 环境下的情形,这样调用时咱们就能够没必要显式的传入 a 的值了,这时候直觉告诉我参数 b 后面的参数应该是 c,然而编译器却不领情。这样看来,即使是在 1.4 当中,咱们也须要确保具名参数和位置参数与形参的位置对应才能在具名参数以后添加位置参数。

所以,我我的的建议是对于参数比较多且容易混淆的情形最好都以具名参数的形式给出,对于参数个数较少的情形则能够所有采用位置参数。在这里还有另外的一个建议就是函数的参数不宜过多,参数越多意味着函数复杂度越高,越可能须要重构。

2.7 优化属性代理的编译

若是你们本身写过属性代理类的话,必定知道 get 和 set 两个函数都有一个 KProperty 的参数,这个参数其实就是被代理的属性。为了获取这个参数,编译器会生成一个数组来存放这代理的属性,例如:

class MyOtherClass {
        val lazyProp by lazy { 42 }
    }
复制代码

编译后生成的字节码反编译以后:

public final class com.bennyhuo.kotlin.MyOtherClass {
      static final kotlin.reflect.KProperty[] $$delegatedProperties;
      static {};
      public final int getLazyProp();
      public com.bennyhuo.kotlin.MyOtherClass();
    }
复制代码

其中 $$delegatedProperties 这个数组就是咱们所说的存被代理的属性的数组。不过,绝大多数的属性代理其实不会用到 KProperty 对象,所以无差异的生成这个数组其实存在必定的浪费。

所以对于属性代理类的 get 和 set 函数实现为内联函数的情形,编译器能够确切的分析出 KProperty 是否被用到,若是没有被用到,那么就不会生成这个 KProperty 对象。

这里还有一个细节,若是一个类当中同时存在用到和没用到 KProperty 对象的两类属性代理,那么生成的数组在 1.4 当中只包含用到的 KProperty 对象,例如:

class MyOtherClass {
        val lazyProp by lazy { 42 }
        var myProp: String by Delegates.observable("<no name>") {
                kProperty, oldValue, newValue ->
            println("${kProperty.name}: $oldValue -> $newValue")
        }
    }
复制代码

其中 myProp 用到了 KProperty 对象,lazyProp 没有用到,那么生成的 $$delegatedProperties 当中就只包含 myProp 的属性引用了。

2.8 参数列表最后的逗号

这个需求别看小,很是有用。咱们来看一个例子:

data class Person(val name: String, val age: Int)
    
    fun main() {
        val person = Person(
            "bennyhuo",
            30
        )
    }
复制代码

Person 类有多个参数,传参的时候就会出现前面的参数后面都有个逗号,最后一个没有。这样看上去好像也没什么问题是吧?那有可能你没有用到过多行编辑:

图 7:多行编辑逗号的问题

这里这个逗号有时候会特别碍事儿,但如何每一行均可以有一个逗号这个问题就简单多了:

图 8:多行编辑全部参数

除了这个场景以外,还有就是调整参数列表的时候,例如我给 Person 在最后加了个 id,我还得单独给 age 的参数后面加个逗号:

图 9:增长参数给原来的参数加逗号

这时候我又以为 id 应该放到最前面,因而作了个复制粘贴,发现仍是要修改逗号。固然,最后的这个功能 IntelliJ 有个快捷键能够直接交换行,同时帮咱们自动处理逗号的问题,不过总体上这个小功能仍是颇有意思的。

提及来,JavaScript 当中的对象字面量当中也容许最后一个字段后面加逗号:

图 10:JavaScript 的对象字面量

不过请注意,尽管它与 JSON 有着深厚的渊源,但 JSON 的最后一个字段后面是不容许加逗号的(固然还有字段要加引号)。

2.9 when 表达式中使用 continue 和 break

continue 和 break 的含义没有任何变化,这两者仍然在循环当中使用,只不过循环内部的 when 表达式当中在以前是不可使用 continue 和 break 的,按照官方的说法,他们以前有意将 continue 或者 break 用做 when 表达式条件 fallthrough 的,不过看样子如今还没想好,只是不想再耽误 continue 和 break 的正常功能了。

2.10 尾递归函数的优化

尾递归函数估计你们用的很少,这里主要有两个优化点

  • 尾递归函数的默认参数的初始化顺序改成从左向右:

  • 尾递归函数不能声明为 open 的,即不能被子类覆写,由于尾递归函数的形式有明确的要求,即函数的最后一个操做必须只能是调用本身,父类的函数声明为 tailrec 并不能保证子类可以正确地按要求覆写,因而产生矛盾。

图 11:1.4 中尾递归函数的默认参数列表初始化顺序

2.11 契约的支持

从 1.3 开始,Kotlin 引入了一个实验特性契约(Contract),主要来应对一些“显而易见”状况下的类型推导或者智能类型转换。

在 1.4 当中,这个特性仍然会继续保持实验状态,不过有两项改进:

  • 支持使用内联特化的函数来实现契约
  • 1.3当中不能为成员函数添加契约,从1.4开始支持为 final 的成员函数添加契约(固然任意成员函数可能存在被覆写的问题,于是不能添加)

2.12 其余的一些改动

除了语法上的明显的改动以外,1.4 当中也直接移除了 1.1-1.2 当中协程的实验阶段的 API,有条件的状况下应该尽快去除对废弃的协程 API 的使用,若是暂时没法完成迁移,也可使用协程的兼容包 kotlin-coroutines-experimental-compat.jar。

剩下的主要就是针对编译器、使用体验的各类优化了,实际上这才是 Kotlin 1.4 最重要的工做。这些内容相对抽象,我就不作介绍了。

补充一点,在本文撰写过程当中,我使用 IntelliJ IDEA 2019.3.3 来运行 Kotlin 1.3,使用 IntelliJ IDEA 2020.1 BETA 来运行 Kotlin 1.4-M1,结果发现后者的代码提示速度彷佛有明显的提高,不知道是否是个人错觉,你们能够自行感觉下并发表你的评论。

3. 小结

Kotlin 目前的语法已经比较成熟了,仍是那句话,提高开发体验,扩展应用场景才是它如今最应该发力的点。

将来可期。

若是你们想要快速上手 Kotlin 或者想要全面深刻地学习 Kotlin 的相关知识,能够关注我基于 Kotlin 1.3.50 全新制做的新课,课程初版曾帮助3000多名同窗掌握 Kotlin,此次更新回归内容更精彩:

扫描二维码或者点击连接《Kotlin 入门到精通》便可进入课程啦!

想要找到好 Offer、想要实现技术进阶的迷茫中的 Android 工程师们,推荐你们关注下个人新课《破解Android高级面试》,这门课涉及内容均非浅尝辄止,目前已经有700+同窗在学习,你还在等什么(*≧∪≦):

扫描二维码或者点击连接《破解Android高级面试》便可进入课程啦!

相关文章
相关标签/搜索