聊聊kotlin1.5和1.6版本提供的一些新特性

6,658 阅读6分钟

大家好,本次的文章主要是聊聊Kotlin1.5和1.6版本提供的一些特性,希望能扩充大家的眼界,如果能对你日常的开发带来帮助,这将是我写这篇文章最大的价值。

一. 挂起函数类型可以作为父类型

在了解这个之前,先看下函数类型做父类型的用法吧:

class Block : () -> String {
    override fun invoke(): String {
        return ""
    }
}

fun test(list: MutableList<() -> String>) {
    list.add(Block())
}

Block类的父类型就是() -> String,并且需要重写方法invoke()方法。当需要调用该类时,可以通过Block().invoke()或者 Block()()实现。

我们将上面的反编译成java看下实现原理:

其实本质上就是实现了Function0这个接口,就那么朴实无华:

如果说我们充当父类型的函数类型为带一个参数的(Int) -> String,那么将会是实现Function1这个接口;以此类推,函数类型每新增一个参数,实现的接口FunctionX中X就要加一,当时这个不能无限新增,目前看到的FunctionX类型的接口最高为Function22

我们可以看下LeakCanary的源码FragmentAndViewModelWatcher,就有用到这个特性:

以上都是普通函数类型作为父类型的讲解,接下来就看下挂起函数类型作为父类型吧,无非就是普通在函数类型前面增加了一个suspend关键字。

class Block : suspend () -> String {
    override suspend fun invoke(): String {
        return withContext(Dispatchers.IO) {
            //模拟耗时
            delay(2000)
            "result"
        }
    }
}

这样我们就可以愉快的invoke方法中调用协程官方api执行一些比如耗时操作了,非常的好用。

发编译下代码,其实可以看到本质上还是实现了Function1接口,为什么挂起来的函数类型明明没有参数,为啥不是实现Function0接口呢?我们看下反编译的代码:

invoke方法会默认新增一个Continuation类型的参数,这个是帮助我们执行挂起函数类型的关键,所以最终实现的接口为Function1

至于挂起的原理,比如上图invoke方法中具体的代码逻辑太复杂了,想要了解协程挂起原理的给大家推荐一篇文章阅读:写给Android工程师的协程指南

想要使用这个特性,要么你的kotlin插件版本升级到了1.6.0及以上,要么你的插件版本为1.5.30并且在build.gradle脚本中增加配置:

二. 稳定的挂起类型转换

在kotlin1.6.0的版本,实现了稳定的普通函数类型到挂起函数类型的隐式转换,说了这么多,接下来我们通过一个例子进行了解:

fun execute(block: suspend () -> Unit) {

}

fun test(block: () -> Unit) {
    execute(block)
}

首先我们声明了一个execute()方法,其中方法的参数为挂起函数类型;然后test()方法参数声明了一个普通函数类型;最后我们在test()方法中成功调用了execute()方法并将普通函数类型的参数传递给了execute()方法。

上面这就完成了一次普通函数类型到挂起函数类型的转换,请注意,这个操作可不能反着来,也就是由挂起函数类型转化成普通函数类型是不可能的,毕竟普通函数类型可没有协程代码的执行环境

官方提供这个特性的目的在笔者看来主要是增加兼容性吧,而且大家不要认为这样转换会增加挂起的开销,毕竟:

因为普通函数类型根本不存在所谓的挂起点,即使转换成挂起函数类型且被执行,也不会执行任何挂起操作的,只是作为一个普通的函数执行并返回而已。

三. 注解类可以被实例化

我们看下java中一个例子:

定义一个注解HuiHui

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface HuiHui {
     String content();
}

这个注解本质上就是一个接口,自然在java中是可以被实现的:

public class HuiImpl implements HuiHui {
    @Override
    public String content() {
        return null;
    }

    @Override
    public Class<? extends Annotation> annotationType() {
        return null;
    }
}

上面的场景在java中是可以的,那我们看下kotlin中的注解可不可以这样搞:

报错了看到了不,提示注解类HuiPlus需要增加open,但是给其增加open就会提示:

除了上面这种写法,这样也会报错的:

总之一句话,kotlin中的注解不支持实例化。Kotlin官方就注意到了这点,这样以后还能和java之间愉快的玩耍嘛,所以在1.5.30插件版本出手了。

简单一句话,注解类支持被实例化,接下来我们通过代码进行实战。

可以看到上面的代码没有丝毫报错。其实我们是可以通过kotlin的反射库是可以调用注解类的构造方法的,但是这样显得繁琐复杂,直接提供一个实例化注解类的特新岂不是更加美哉?

一般如果我们想了解kotlin是如何实现这种能力的,都会尝试反编译成java代码看下。但遗憾的是,对于kotlin注解实例化的细节被隐藏掉了,反编译成java也没有帮助,隐藏掉的好处是允许编译器做一些隐式的优化工作

如果想要使用这个特性,和参考上面第一节内容末尾的操作。

四. 支持对类泛型声明注解

解释起来很麻烦,直接给大家上代码:

@Target(AnnotationTarget.TYPE_PARAMETER)
annotation class FLAG

class Annotation2<@FLAG T> {
    val content: String = ""
}

可以看到我声明了一个FLAG注解,并且通过@Target(AnnotationTarget.TYPE_PARAMETER)限制只能给泛型类型修饰。

这个特性相当于把我们注解的范围更加颗粒化了,允许我们以在更加细致的维度中去处理问题。而且,请注意,这个注解最终是会被编译到jvm字节码的,所以通过Android自定义注解处理器是可以拿到并处理该注解的。 这样就更加扩展了该特性的使用范围。

如果想要使用这个特性,和参考上面第一节内容末尾的操作。

五. 长期支持对kotlin插件旧版本的维护

从kotlin1.6.0开始,kotlin官方将长期支持维护对当前稳定版本的过去三个旧版本的维护,换句话说,如果当前稳定版本为1.6.0,那么对于kotlin1.3、kotlin1.4和kotlin1.5也是继续进行维护的。

六. 总结

本篇文章主要是对kotlin1.5和1.6版本新增的特性做了一些讲解,小而美的那种。我这里并没有列举全所有新增的特性,那样对于大家和我都是一个负担,只是拿出一些比较典型的特性分享给大家。

请注意,如果由某个特性非常适合展开讲讲,以及对于我们开发很有帮助的话,比如@Jvm-default=all、@BuilderInference等等这些特性,我一般都是专门出一篇文章针对这些特性进行讲解的,大家感兴趣可以看下下面的历史文章列表。

七. 历史文章

@JvmDefaultWithCompatibility优化小技巧,了解一下~

优化@BuilderInference注解,Kotlin高版本下了这些“毒手”!

kotlin密封sealed class/interface的迭代之旅

八. 参考列表

kotlinlang.org/docs/whatsn…

kotlinlang.org/docs/whatsn…

hub.nuaa.cf/Kotlin/KEEP…

youtrack.jetbrains.com/issue/KT-25…


本文正在参加「金石计划」