33

Kotlin修炼指南(四)

 3 years ago
source link: https://blog.csdn.net/eclipsexys/article/details/108744552
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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集合做加减运算,如图所示。

f6bm6fu.png!mobile

代码如下所示。

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}
}

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

eIzQBjf.png!mobile

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

uq6zi2m.png!mobile

很明显,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的内联函数,如下图所示。

6NRFfqe.png!mobile

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

延迟执行 (lazily) 的Sequence类型

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

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

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

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

6Nbe6j7.png!mobile

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

Fv2MZ3f.png!mobile

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

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更高效


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK