本文将讨论一些 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 = Int
typealias L = Long
typealias S = String
fun main() {
val i: I = 5
val l: L = 5L
val s: S = "s"
}
可以看到我们给我们常见的几种类型 Int
,Long
和 String 分别
指定了一个别名 I
, L
和 S
,因为我们在 main
方法里面声明的时候便可以使用 I
, L
, S
来代替真正的类型。
接下来看看函数类型:
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 branch
val result = when (color) {
Color.Green -> { }
Color.Red -> { }
Color.Yellow -> { }
}
// or use else
val 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)
}
我们实例化一个 InlineClass
,decompile
之后发现真正的代码长这样:
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