专栏首页Android群英传Kotlin修炼指南(四)

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

本文分享自微信公众号 - Android群英传(android_heroes),作者:徐宜生

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2020-09-22

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 当前移动行业之拙见

    用户1907613
  • Kotlin修炼指南(三)——奇技淫巧

    Kotlin作为Android开发的首选语言,为开发者提供了大量的语法糖和技巧,让开发者可以专注于需求开发,而将语言所带来的影响减少到最少。Java和Kotli...

    用户1907613
  • FlutterDojo设计之道—状态管理之路(一)

    在这个Widget Tree中,通常会存在很多组件之间的相互依赖,时间一长,就很容易变成下面这样。

    用户1907613
  • 【Spark Mllib】分类模型——各分类模型使用

    这个数据集源自 Kaggle 比赛,由 StumbleUpon 提供。比赛的问题涉及网页中推荐的页面是短暂(短暂存在,很快就不流行了)还是长久(长时间流行)。

    用户1621453
  • 微服务实践之客观认识微服务

    微服务由MicroServices翻译而来,是一种软件架构风格,它以专注于单一责任与功能的小型功能区块为基础,利用模组化的方式组合出复杂的大型应用程序,各功能区...

    嘉为科技
  • 严肃探讨:苹果万亿美金身家怎么花?

    美东时间8月2日中午11点48分左右,苹果股价直抵207.05美元,成为了美国历史上首个市值突破1万亿美元的上市公司。

    镁客网
  • 腾讯云 Serverless 技术在「老司机」的落地实践

    首先简单介绍下,我们是一个有趣、有态度的汽车新媒体分享平台,我们有自己的 APP 和网站。目前服务超过 2 亿的汽车消费者与汽车兴趣用户群体,为广大汽车用户提供...

    腾讯云serverless团队
  • 源码安全:悬在大厂头上的达摩克利斯之剑

    从 B 站源码泄露开始到 GitHub 最终删除代码的两小时,大概是今年 B 站最煎熬的时刻,以至于他在向 Github 求助删除的 DMCA 邮件中,在 Pl...

    CODING研发管理系统
  • TextView的setCompoundDrawables和setCompoundDrawablesWithIntrinsicBounds的区别

    我们都只TextView支持设置文字和图片同时显示,通常会联想到两种方法,一种是直接设置drawableXXX(Left, Top, Right, Bottom...

    包子388321
  • 为什么数据中台如此重要?明略科技吴信东:智能时代企业核心竞争力之源 | MEET2020

    数字化转型无疑成了当下一个不可阻挡的大浪潮。而在今年,中台这两个字随着阿里巴巴、腾讯、华为等巨头的战略部署,一下子火了起来。

    量子位

扫码关注云+社区

领取腾讯云代金券