阅读 2084

Kotlin修炼指南(四)

Kotlin这门语言极其灵活,这是一把双刃剑,相比Java,大家写的都是白话文,不论水平高低,大家基本都是能非常流畅的阅读彼此的代码的,但是在使用Kotlin之后,由于大家的Kotlin表达水平和思维习惯的不同,就好造成这样一种情形,「这tm还能这样写?」、「这写的是个啥?」、「卧槽、牛B」。

所以下面总结了一些平时写Kotlin时,那些跟Java白话文写的不太一样的地方,拓展拓展大家的思维,让开发者在写Kotlin代码的时候,能够更加的有Kotlin味儿。

Sealed Class

Sealed Class,听上去很高端,密封类,实际上并不难理解,它密封的是逻辑,作用就是可以让逻辑更加完善、严谨。

举个很常见的例子,在网络请求中有两种状态,Success和Fail。

open class Result

class Success(val msg: String) : Result()
class Fail(val error: Throwable) : Result()

fun getResult(result: Result) = when (result) {
    is Success -> result.msg
    is Fail -> result.error.message
    else -> throw IllegalArgumentException()
}
复制代码

在判断的时候,可以使用when来进行判断,但是必须有else条件,这就导致了网络请求的状态出来三种状态,即Success、Fail和else,这样一不利于逻辑的完整性,也容易在状态很多的时候漏掉一些状态的判断。

所以,Kotlin提供了Sealed Class来解决这个问题,避免使用when的时候,出现这种无用的判断分支。代码如下所示。

sealed class Results
class Success(val message: String) : Results()
class Failure(val error: Exception) : Results()

fun getMessage(result: Results) = when (result) {
    is Success -> println(result.message)
    is Failure -> println(result.error.toString())
}
复制代码

这样可以在when的时候通过快捷键自动罗列所有的场景。

更加复杂的,还可以使用Sealed Class来创建嵌套的密封逻辑,例如前面的Error中,还可以封装更为详细的Error类型,在这样的场景下,Sealed Class的优势就能更一步体现出来了,代码如下所示。

sealed class Result {
    data class Success(val message: String) : Result()
    sealed class Error(val error: Exception) : Result() {
        class SystemError(exception: Exception) : Error(exception)
        class AuthError(exception: Exception) : Error(exception)
    }

    object NoResponse : Result()
}

fun getMessage(result: Result) = when (result) {
    is Result.Success -> println(result.message)
    is Result.Error.SystemError -> println(result.error)
    is Result.Error.AuthError -> println(result.error)
    Result.NoResponse -> println(result)
}
复制代码

在写了when函数之后,只要判断的条件是一个Sealed Class,那么都可以通过快捷键自动补全,生成所有的枚举条件,这可比你自己去列举靠谱多了,特别是像这种嵌套的情况。

在Android中,除了网络请求这种比较常用的场景外,View的点击的封装,也是比较常用的例子。

例如一个RecyclerView Item的点击事件,可以封装一个ItemClick的Sealed Class,这个类中密封了ShareClick,FavoriteClick,DelClick等逻辑,通过设置点击监听,handle不同的点击事件。

Sealed Class的核心就是,用一组清晰明确的类型,将结果分配给每个密封状态,在保存逻辑的严谨性的同时,减少垃圾代码的产生。

操作符重载

操作符重载可以让开发者在原本没有操作符功能的函数中,为其新增操作符含义的功能。

操作符重载是各种骚操作的来源,更是一些别有用心者的万恶之源

例如官方给出的例子,利用 plus (+) 和 minus (-) 对Map集合做加减运算,如图所示。

代码如下所示。

fun main() {
    val numbersMap = mapOf("one" to 1, "two" to 2, "three" to 3)

    // plus (+)
    println(numbersMap + Pair("four", 4)) // {one=1, two=2, three=3, four=4}
    println(numbersMap + Pair("one", 10)) // {one=10, two=2, three=3}
    println(numbersMap + Pair("five", 5) + Pair("one", 11)) // {one=11, two=2, three=3, five=5}

    // minus (-)
    println(numbersMap - "one") // {two=2, three=3}
    println(numbersMap - listOf("two", "four")) // {one=1, three=3}
}
复制代码

集合中本没有「+」、「-」操作,但是可以通过重载操作符,给集合类型的变量增加这样的功能,这样写起来更加方便,除了常见的「+」、「-」操作以外,下面这些操作符都可以被重载。

那么重载操作符到底是怎么实现的呢?Java中好像并没有这种功能,所以,Kotlin一定是通过编译器的黑魔法来实现的,通过反编译Kotlin的代码,可以发现这个黑魔法。例如上面Map的plus重载运算符,在反编译之后的代码如下所示。

很明显,Kotlin就是在编译的时候,把重载的操作符替换成了前面定义的函数,实际上有点类似拓展函数的实现,所以Java其实本身不支持重载操作符,但是Kotlin通过编译器来实现了操作符的重载。

拓展in的操作符

in操作符具有很强的语义性,所以在自定义的类中,重载in操作符,可以简化很多操作,特别是在when条件判断中,例如在Collection中,Kotlin就重载了in操作符,提供了更加方便的判断,代码如下所示。

fun main() {
    when (val input = "xuyisheng") {
        in listOf("xuyisheng", "zhujia") -> println("result $input")
        in setOf("zj", "rkk") -> println("result $input")
        else -> println("result not found")
    }
}
复制代码

那么我们可以模仿Kotlin官方的做法,在自定义的类中重载in操作符,例如给正则增加in操作符,用来判断匹配类型,代码如下所示。

operator fun Regex.contains(text: CharSequence): Boolean {
    return this.containsMatchIn(text)
}

fun main() {
    when (val input = "abc") {
        in Regex("[0–9]") -> println("contains a number")
        in Regex("[a-zA-Z]") -> println("contains a letter")
    }
}
复制代码

通过这种方式,语义更加明确,代码也更加简洁。

操作符重载一定要慎用,防止有些人重载「+」为「-」,导致代码难以理解。

集合操作

在Kotlin中,集合有两种类型,即Collection和Sequence,在Java中,我们很少提及有两种集合类型,以至于在写Kotlin的时候,对它提供的这两种集合类型傻傻分不清楚。但在Kotlin的函数式编程世界里,它们的区别是非常大的。

立即执行 (eagerly) 的Collection类型

Collection,是我们最长用的集合类型,甚至成了集合的代名词,它的特点如下。

  • 每次操作时立即执行的,执行结果会被存储到一个新的集合中
  • Collection中的转换操作是内联函数。例如map函数的实现方式,它是一个创建了新ArrayList的内联函数,如下图所示。

这也是通常在使用Collection的函数式编程方式时,内存使用更大的原因。

延迟执行 (lazily) 的Sequence类型

Sequence,也是集合的一种,但是被Collection抢了翻译,所以只能叫做序列,它跟Collection最大的区别就是,Sequence是延迟执行的。

它有两种类型: 中间操作 (intermediate) 和末端操作 (terminal)。中间操作不会立即执行,它们只是被存储起来,仅当末端操作被调用时,才会按照顺序在每个元素上执行中间操作,然后执行末端操作。

中间操作 (比如 map、distinct、groupBy 等) 会返回另一个Sequence,而末端操作 (比如 first、toList、count 等) 则不会。

同样是map函数,在Sequence中,像map这样的中间操作是将转换函数会存储在一个新的Sequence实例中,如图所示。

而例如first这样的末端操作,则会真正执行具体的操作。例如first,则会对Sequence中的元素进行遍历,直到找到预置条件匹配为止,代码执行如下所示。

下面通过一个例子来演示下这两种集合类型的操作异同。

data class People(val name: String, val age: Int)

val xuyisheng = People("xuyisheng", 18)
val zhujia = People("zhujia", 3)
val rkk = People("rkk", 28)
val zj = People("zj", 38)

val list = listOf(xuyisheng, zhujia, rkk, zj)

fun main() {
    val testCollection = list.map {
        it.copy(age = 1)
    }.first {
        it.name == "xuyisheng"
    }
    println(testCollection)

    val testSequence = list.asSequence().map {
        it.copy(age = 1)
    }.first {
        it.name == "xuyisheng"
    }
    println(testSequence)
}
复制代码

首先,我创建了一个List,默认为Collection类型,通过asSequence函数,可以将其转换为Sequence。下面分别针对这两种方式来看下具体的代码执行的流程。

Collections执行过程

  1. 调用map函数时会创建一个新的ArrayList。Kotlin会遍历初始Collection中所有项目,并复制原始的对象,并将每个元素的age值改为1,再将其添加到新创建的列表中。

  2. 调用first函数时,会遍历每一个元素,直到找到第一个符合条件的元素。

Sequences执行过程

  1. 调用asSequence函数创建一个基于原始集合的迭代器创建一个Sequence。
  2. 调用map函数,这是一个中间操作,所以Sequence会将转换操作的信息存储到一个列表中,该列表只会存储要执行的操作,但并不会执行这些操作。
  3. 调用first函数时,这是一个末端操作,所以它会将中间操作作用到集合中的每个元素。我们遍历初始集合和之前存储的操作列表,对每个元素执行map操作,然后继续执行first操作,当遍历到符合条件的数据时,就完成了操作,所以就无需在剩余的元素中进行map操作了。

综上所述,它们的差异如下。

  • 使用Sequence是不会去创建中间集合的,但会创建中间操作集合,在执行末端操作时,由于Item会被逐个执行,所以中间操作只会作用到部分Item上。

  • Sequence每个元素被依次验证,Collection每个操作都将作用在整个集合,每个操作都将创建新的集合。

  • Collection会为每个转换操作创建一个新的集合,而Sequence仅仅是保留对转换函数的引用。

Collection的操作使用了内联函数,所以处理所用到的字节码以及传递给它的lambda字节码都会进行内联操作。而Sequence不使用内联函数,因此,它会为每个操作创建新的Function对象。

使用场景

针对Collection和Sequence的这种差异,我们需要在不同的场景下,选择不同的集合类型。

  • 数据量小的时候,其实Collection和Sequence的使用并无差异
  • 数据量大的时候,由于Collection的操作会不断创建中间态,所以会消耗过多资源,这时候,就需要采用Sequence了
  • 对集合的函数式操作太大,例如需要对集合做map、filter、find等等操作,同样是使用Sequence更高效