本文将讨论一些 Kotlin 冷门的 API 或者特性,这些知识掌握与否并不影响我们编程,因为毕竟这些东西在 Java 根本就是没有的。但是这些冷门的知识在某些场景却能解决一些痛点,因此还是值得一学的。

文章开始要从一个非常有意思的 Github 开源库说起,地址在这里:https://github.com/saket/ReuserView。该库作者似乎对 RecyclerView 的命名很不满意。我们可以在 README.me 看到作者的观点:

你可以把 ViewHolder 当作一个塑料瓶, bindViewHolder() 传入的实体是美味的可乐。当你把瓶中的可乐换成普通的水,你是在复用这个瓶子; 而如果你把瓶子回收掉,让他作为 Google Home mini 的材料,你是在回收这个瓶子。因此,复用和回收是两件很不同的事情!

所以这个库就帮我们纠正了这件事情,我们可以在项目中使用这个库:

"implementation "me.saket.reuserview:reuserview:4.1.2021"

然后我们就可以完全使用 ReuserView 来代替 RecyclerView。注意这个库的版本号也是非常的滑稽,4.1 代表愚人节。下面我们来进行一波源码分析。

惊人的发现整个库就一个 ReuserView.kt 文件,然后代码也就两行:

package androidx.recyclerview.widget
typealias ReuserView = RecyclerView

就是给我们的 RecyclerView 起了个别名叫 ReuserView,是不是有种被骗的感觉,哈哈。

其实这里就引出本文要讨论的第一个 Kotlin 中相对冷门的特性 -- typealias

1. typealias

Typealias 允许我们给Kotlin 中的任意一个类型指定一个别名,甚至包括函数类型,带泛型的类型,这样当我们其它地方需要用到这个类型的时候,我们可以用其别名替代。

看个最简单的例子:

typealias I = Inttypealias L = Longtypealias S = String
fun main() {    val i: I = 5    val l: L = 5L    val s: S = "s"}

可以看到我们给我们常见的几种类型 IntLong 和 String 分别指定了一个别名 IL 和 S,因为我们在 main 方法里面声明的时候便可以使用 ILS 来代替真正的类型。

接下来看看函数类型:

typealias STB = (String) -> Boolean
private fun test(stb: STB) {}
fun main() {    val stb: (String) -> Boolean = { str: String ->        true    }    test(stb)}

我们把给 String -> Boolean 这样一个函数类型指定一个别名为 STB,然后我们写了一个 test 方法接受一个 STB 类型参数,我们可以看到在 main 方法里面,我们实例化了一个 String -> Boolean 这样一个函数类型,然后调用了 test,将其作为实际参传入,这完全是没有问题的。

接下来看复杂一点的例子,带泛型的类型参数。

typealias MListToMList<K, V> = MutableMap<MutableList<K>, MutableList<V>>
private fun test(mListToMList: MListToMList<String, Int>) {}
fun main1() {    val mListToMList = mutableMapOf(mutableListOf<String>() to mutableListOf<Int>())    test(mListToMList)}

可以看到在这种场景下,typealias 的灵活度和简化代码的能力还是比较突出的。

2. exhaustive when

Kotlin 的 when 的表达式类似于 Java 的 switch case 语句。

假如我们有一个 enum class:

enum class Color {    Green, Red, Yellow}

当我们的 when 仅仅作为一个语句时,当我们在判断一个 Color 类型时,不必书写所有的 branch,即使会收到一个 Lint 提示,但是还是可以正常编译的。

// it's okwhen(color){    Color.Green -> { }}

但当 when 作为一个表达式时,我们必须书写所有的 branch 或者使用 else, 就像下面这样, 否则我们的编译器就会报错:

// compile error!!!!val result = when (color) {    Color.Green -> { }}
// write all branchval result = when (color) {    Color.Green -> { }    Color.Red -> { }    Color.Yellow -> { }}
// or use elseval result = when (color) {    Color.Green -> { }    else -> { }}

我们把保证 when 所有的 branch 都得到处理的特性称为 “exhaustive”。

事实上当我们的 branch 越来越多时,即使 when 作为语句而不是表达式,保持 exhaustive 也是有必要的,可以避免一些不必要的错误。所以我们可以欺骗编译器,假装我们的 when 是一个表达式而不是语句,这样编译器就必须强制我们把 when 实现成 exhaustive 的。

如何实现呢?我们可以定义一个扩展属性,返回自身就行了。

val <T> T.exhaustive: T    get() = this

然后我们使用 when 时,在尾部主动的调用这个扩展属性。

when (color) {    Color.Green -> { }}.exhaustive

此时,when 语句最后因为调用了扩展属性,被识别为一个表达式,因此编译器会强制我们将 when 写成 exhaustive

这样,如果我们没有书写所有的 branch, 一个 Lint 升级成了 Compile Error,使得开发者更加不容易犯错误。

3. destrcturing declarations

当你看到你的同事写出如下这样的代码的时,不要觉得惊讶,他只是用到 Kotlin 的 destrcturing declarations 特性。

data class Person(val name: String, val age: Int, val address: String)
fun main() {    val person = Person("name", 0, "address")    val (name, age, address) = person
   assert(name == "name")    assert(age == 0)    assert(address == "address")}

上面的代码等价于:

fun main() {    val person = Person("name", 0, "address")    val name = person.component1()    val age = person.component2()    val address = person.component3()
   assert(name == "name")    assert(age == 0)    assert(address == "address")}

对于 data class,编译器为每个在主构造方法中声明的属性生成一个 componentN 的函数,用于返回该属性,其中 N 为从左到右的序号,从 1 开始。

你可以手动的在自己的类中实现 componentN 的方法让自己的类支持这个功能。

destrcturing declarations 同样适用于集合。

比如我们要获取集合的前几个元素,我们可以这样书写我们的代码:

fun main() {    val list = listOf<Int>(0, 1, 2)    val (first, second, third) = list}

其等价于:

fun main() {    val list = listOf<Int>(0, 1, 2)    val first = list.get(0)    val second = list.get(1)    val third = list.get(2)}

Kotlin 内置的 Pair 以及 Tripple 类都支持 destrcturing declarations ,这在我们函数需要返回多个值的情况下有很大帮助。

假如我们有一个方法返回两个参数。

fun funReturn2Result(): Pair<Int, Int> {    return 1 to 2}

调用处我们可以很方便的获取到多个返回值

fun main() {    val (first, second) = funReturn2Result()}

而不是这样:

fun main() {    val result = funReturn2Result()    val first = result.first    val second = result.second}

4. inline class

你可能听说过 inline function,但是啥是 inline class, 有啥用?

当我们定义一个 class 时,我们可以在前面加上 inline 关键字。

inline class InlineClass(val str:String)

这个 class 就成为了一个 inline class。注意: inline class 有且只能仅有一个属性。

先来感受下 其 “inline” 特性。

inline class InlineClass(val str:String)
fun main() {    val inlineClass = InlineClass("hello world")    println(inlineClass.str)}

我们实例化一个 InlineClassdecompile 之后发现真正的代码长这样:

public static final void main() {   String inlineClass = InlineClass.constructor-impl("hello world");   System.out.println(inlineClass);}

我们并没有去实例化一个 InlineClass,而是调用这个类的静态方法 constructor-impl 返回了一个字符串。

我们看看 constructor-impl 长啥样:

@NotNullpublic static String constructor_impl/* $FF was: constructor-impl*/(@NotNull String str) {   Intrinsics.checkParameterIsNotNull(str, "str");   return str;}

constructor_impl 方法粗暴的返回了传入的 str

在上面这个例子,我们发现 inline class 在编译后,并不会实例一个 class,节省了内存开销。那么我们直接用 String 类型不就好了吗,为什么要让编译器来帮我做个优化?

其实 inline class 主要的作用是可以类型化参数。

假如我们有一个方法:

fun login(userName: String, password: String) {
}

userName 和 password 都拥有同样的类型 String.

这导致某种情况下,我们将调用顺序写反也可以正常通过编译。

fun main() {    val userName = "userName"    val password = "password"K    // it's ok    login(userName, password)    // it's also ok    login(password, userName)}

对于这种情况,我们可以通过 inline class 来避免。

我们可以定义 2 个 inline class , UserName 和 Password,然后修改我们的 login 方法。

inline class UserName(val userName: String)inline class Password(val password: String)fun login(userName: UserName, password: Password) {
}

这样在调用处的使用变成了这样:

fun main() {    val userName = "userName"    val password = "password"      // it's ok    login(UserName(userName), Password(password))
   // compile error    login(Password(password), UserName(userName))}

此时,因为 password 和 userName 被附加了类型,相反的顺序将不会通过编译。

5. sequence

Kotlin 天然支持 stream-API,而不像 Java 一样在 1.8 才支持。所以切换到 Kotlin 我们会很自然的在集合上面使用 stream-API,就像下面这样:

fun main() {    val list = listOf<Int>(0, 1, 2)    list        .map { it * it }        .filter { it > 3 }        .forEach {            print(it)        }}

然而在某些场景,使用 sequence 而不是集合可以提升性能。

啥是 sequence ?事实上 sequence 和 集合很相似,同时 sequence 也支持 stream-API

拿上面的例子来说,我们可以调用集合的 asSequence 方法将其转化为一个序列,然后无需更改其余的地方,其运行效果是一样的。

fun main() {    val list = listOf<Int>(0, 1, 2)    list        .asSequence()        .map { it * it }        .filter { it > 3 }        .forEach {            print(it)        }}

那么使用集合和 sequence 的区别在哪里呢?

在使用操作符的地方加上注释标明当前调用的返回值之后,你能感受到其中的区别。

使用集合:

fun main() {    val list = listOf<Int>(0, 1, 2, 3)    list        .map { it * it } // List<Int>        .filter { it > 3 } // List<Int>        .forEach {            print(it)        }}

使用集合时,每使用一次 operator 会产生一个中间集合。

使用序列时:

fun main() {    val list = listOf<Int>(0, 1, 2, 3)    list        .asSequence()  // Sequence<Int>        .map { it * it } // Sequence<Int>        .filter { it > 3 } // Sequence<Int>        .forEach {            print(it)        }}

使用序列时,每次使用 operator 并不会产生一个中间集合,只会产生一个新的 Sequence 对象,它仅仅是持有了上一个 Sequence 对象。

想象一下如果我们的集合的 size 十分庞大,那么每次都生成中间集合带来的开销是很大的,而 Sequence 可以避免这个问题。

Sequence 是惰性的。拿上面的例子来说,如果我们把集合版本的 forEach 去掉,仅仅只是 forEach 这一步不会执行。而如果把序列这一步的 forEach 去掉,那么序列版本中的 map 和 filter 也不会执行。

这一特性给序列带来另一个集合很难优雅做的功能,那就是实现无限制数量的发射器。

比如我们可以写一个用序列实现的计算 1 到 N 的和的功能:

fun getSumOf1ToN(n: Int): Int {   return generateSequence(1) { lastElement ->        lastElement + 1    }        .take(n)        .sum()}

这里使用了 generateSequence 这个 API 来构建一个序列,它将接受一个初始值作为序列的第一个元素,然后通过前一个元素计算下一个元素。如果使用集合来实现,你可能需要每次动态的生成 N 个长度的集合,然后逐个填充,十分的不优雅。

结尾

以上就是最近总结了 5 个 Kotlin 的冷门的 API 或者特性。私以为自己使用 Kotlin 四年多的时间,已经对 Kotlin 足够掌握和了解了,但是在总结这些知识点时,还是有很多不明白的地方,查阅了大量的资料和文档。

由此看出,Kotlin 虽然简洁但是深邃呀。

https://mp.weixin.qq.com/s/_qBZ9UtflpzidLgGD6fFgw