kotlin的语法糖(操作符)🍬

认识kotlin中的let、with、run、also、apply、map、flatMap等操作符。

从java转到kotlin遇到的第一个障碍就是kotlin自带的操作符,在看别人代码的时候总是被各种各样的操作符弄的一头雾水。为什么他可以这么写?为什么他可以直接使用对象的属性?这一系列的代码执行之后到底变成了什么样子?伴随着各种各样的问题,我们不得不先学习一下kotlin的操作符。

本文对kotlin中常用的操作符进行举例说明,方便开发者理解使用。

1. 基础操作符

1.1 let

将调用者传入代码块中,以it指代传入的对象,执行代码块的代码,将代码块最后一行结果或return指定的数据返回。

let的执行效果和把代码写在代码块外面差不多,主要的作用是可以对test变量是否为空做出判断,如果test为空则不会执行代码块中的代码。对于代码的阅读性有一定的提升,业务逻辑和临时变量都写在代码块中,方便区分。

test1("test1")

private fun test1(input: String?) {
    // 返回一个字符串中的第一个数字字符所对应的数字,找不到则返回null
    val result = input?.let { // 如果input为空,则不会执行let代码块的代码,直接返回null
        var number: Int? = null
        for (i in it.iterator()) { // 调用传入对象的方法需要使用it引用
            if (Character.isDigit(i)) {
                number = Integer.parseInt(i.toString())
                break
            }
        }
        number
    }
    LogUtil.print("result = $result") // result = 1
}

1.2 with

传入一个对象,在对象内部执行代码块中的代码,可以直接调用传入对象的公共方法及属性,也可以使用this指代传入对象进行操作,将代码块最后一行结果或return指定的数据返回。

with操作符不好用,他无法以链式调用的方式承接上面的数据,如果传入的对象可能为空,在使用的时候依旧需要对空指针进行判断。一般在需要重复多次调用同一个对象时可以使用这个操作符,可以省去调用对象的名称。

test2("test2")

private fun test2(input: String?) {
    // 返回一个字符串中的第一个数字字符所对应的数字,找不到则返回null
    val result = with(input) {
        if (this == null) {
            return@with null
        }
        var number: Int? = null
        for (i in iterator()) { // 此处可以直接调用String.iterator()方法
            if (Character.isDigit(i)) {
                number = Integer.parseInt(i.toString())
                break
            }
        }
        number
    }
    LogUtil.print("result = $result") // result = 2
}

1.3 run

将调用者传入代码块中,在调用者内部执行代码,可以直接调用传入对象的公共方法及属性,也可以使用this指代传入对象进行操作,将代码块最后一行结果或return指定的数据返回。

run操作符是letwith的结合体,将他们的优点集中到一起,既可以插入到链式调用中,又能直接在代码块中调用传入对象的公共方法及属性,而且在调用前进行空指针判断也很方便。

test3("test3")

private fun test3(input: String?) {
    // 返回一个字符串中的第一个数字字符所对应的数字,找不到则返回null
    val result = input?.run { // 如果input为空,则不会执行run代码块的代码,直接返回null
        var number: Int? = null
        for (i in iterator()) { // 此处可以直接调用String.iterator()方法
            if (Character.isDigit(i)) {
                number = Integer.parseInt(i.toString())
                break
            }
        }
        number
    }
    LogUtil.print("result = $result") // result = 3
}

1.4 also

将调用者传入代码块中,以it指代传入的对象,执行代码块的代码,代码执行完成后将调用对象返回。

alsolet的使用方法和执行效果差不多,唯一的区别是also返回的是调用者本身。

test4("test4")

private fun test4(input: String?) {
    // 创建一个内容为输入字符串,字号为20sp,颜色为白色的TextView
    val textView = TextView(this)
    val result = textView.also { // 此处可以直接将also连接在构造函数后,能够减少一个临时变量
        it.text = input ?: ""
        it.textSize = 20f
        it.setTextColor(0xFFFFFFFF.toInt()) // 最终的返回值为调用者,并不是最后一行代码的值
    }
    LogUtil.print("result = ${result.text}") // result = test4
}

1.5 apply

将调用者传入代码块中,在调用者内部执行代码,可以直接调用传入对象的公共方法及属性,也可以使用this指代传入对象进行操作,执行代码块的代码,代码执行完成后将调用对象返回。

apply是对also的升级,调用方法和属性时不用再使用it调用。也可以看作是run的变种,使用方法和run一致,最终返回传入的对象。apply常用于设置一个对象的多个属性,对于不支持链式调用的对象,可以提供一个类似链式调用的效果。

test5("test5")

private fun test5(input: String?) {
    // 创建一个内容为输入字符串,字号为20sp,颜色为白色的TextView
    val textView = TextView(this)
    val result = textView.apply { // 此处可以直接将apply连接在构造函数后,能够减少一个临时变量
        text = input ?: ""
        textSize = 20f
        setTextColor(0xFFFFFFFF.toInt()) // 最终的返回值为调用者,并不以最后一行代码的值为准
    }
    LogUtil.print("result = ${result.text}") // result = test5
}

1.6 forEach & forEachIndexed

遍历一个列表,对实现Iterable接口的对象进行遍历,将列表中的每一个数据提取出来传递到代码块中,forEach会将数据用it指定并传入代码块中,forEachIndexed则会多传递一个index,用于标记当前数据的位置。

forEach和forEachIndexed并不会返回任何数据

val list = listOf(1, 2, 3, 4, 5)
list.forEach {
    LogUtil.print(it)
}

list.forEachIndexed { index, i ->
    LogUtil.print("$index - $i")
}

1.7 小结

  • 以上“在对象内部执行代码”的说法是方便开发者理解,实际的执行位置并不在对象内部,所以只能调用对象的公共方法及属性,但代码书写方式却和在对象内部书写私有方法一样。
  • letrunapplyalso操作符直接写在函数中时,调用者为函数所在对象。
  • letwithrun均是以闭包形式执行,返回的数据为return数据或最后一行代码的值。
  • applyalso的返回值均是调用者自身。
  • 一般情况下使用runapply就足以满足业务需求,其他三个操作符了解运行效果,能够读懂别人的代码即可。

2. 流程操作符

以上的基础操作符也可用于流程中的数据处理。
在kotlin之前使用过RxJava,kotlin的流程操作符和RxJava差不多,在开发过程中可以直接使用kotlin内置的操作符而不需要再引入第三方库了。

2.1 map

一对一的转换,将n个数据的列表转换成n个数据的列表,类型及数据都可以变换。仅适用于列表或可以转换成列表的数据,准确的说是实现了kotlin.collections.Iterable<T>接口的对象(例如:String会转换成List<Char>进行处理)。map操作符会把列表中的每一个数据提取出来,用it指定,然后执行代码块中的代码,返回return指定的数据或最后一行代码的值。

当我们需要依次处理一个列表中的每个数据的时候就可以使用map操作符,相当于java的for-each循环。和RxJava中的map效果一样。这个流程对数据的数量不会有影响。

val inputList = listOf(5, 4, 3, 2, 1) //创建一个包含5个数字的列表,类型为List<Int>
val result = inputList.map { // it指代当前处理的数据
    if (it == 1) {
        return@map "first"
    }
    "index_$it"
}
LogUtil.print("result = $result") // result = [index_5, index_4, index_3, index_2, first]

示例中输入的是5个int数字,我们通过判断将值为1的数字修改为“first”,其余数字则添加“index_”前缀,最终输出的是一个字符串数组。建议返回同样类型的数据,这样后续继续处理也会方便一些,如果返回的数据类型不一致,得到的列表类型会是Any,不方便继续处理数据。

2.2 flatMap

一对多的转换,将n个数据的列表根据处理逻辑转换成m个数据的列表,类型及数据都可以变换。使用要求和方式与map一样,但代码块中返回的结果要求是一个列表。最终的结果是将所有返回列表的数据连到一起,组成一个新列表。

flatMap对返回列表的数据个数不做限制,我们可以通过flatMap操作符调整列表中数据的个数,也可以将细分的数据提到上层处理。当我们需要把一些对象中的子数据提取到一个列表中时,使用flatMap就很方便。

val inputList = listOf(5, 4, 3, 2, 1) //创建一个包含5个数字的列表,类型为List<Int>
val result = inputList.flatMap {
    if (it <= 1) {
        return@flatMap listOf("$it")
    }
    val index: MutableList<String> = mutableListOf()
    for (i in 1..it) { // 这里把传入的数据当作循环次数使用,如果传入数据是个数据模型,也可以直接提取其中的列表数据。
        index.add("$it-$i")
    }
    index
}
LogUtil.print("result = $result")
// result = [5-1, 5-2, 5-3, 5-4, 5-5, 4-1, 4-2, 4-3, 4-4, 3-1, 3-2, 3-3, 2-1, 2-2, 1]

示例中的MutableList是一个可变列表,kotlin中分为可变列表和不可变列表,当需要动态修改列表数据个数的时候就要使用可变列表。我们通过flatMap操作符对原始列表进行展开处理,最终的结果是将我们每次返回的列表整合成一个新的列表。

2.3 use

可以自动关闭使用的资源,针对的是实现了Closeable接口的数据。use操作符会把使用的对象传递到代码块中,用it指定,然后执行代码块中的代码,返回return指定的数据或最后一行代码的值。

使用这个操作符可以代替传统的try-catch-finally代码块,而且不会影响流式代码的结构。每次使用需要手动关闭的对象时就可以使用use操作符简化代码了。

BufferedReader(InputStreamReader(FileInputStream(File("a.txt")), Charsets.UTF_8)).use {
    val content = it.readLine()
    LogUtil.print(content)
}

这个示例展示了一个按行读取文件的效果,先后构建了FileFileInputStreamInputStreamReaderBufferedReader,最终通过use操作符自动关闭了所有的资源。(BufferedReader在关闭的时候会自动关闭引用的InputStreamReader,所以对最外层的BufferedReader使用use即可)。

对于按行读取文件的功能,kotlin已经提供了相应的扩展方法,该方法也是基于use实现的自动关闭功能,直接调用该方法即可。

File("a.txt").readLines().forEach {
    LogUtil.print(it)
}

3. 总结

kotlin的各种操作符基本上都是通过扩展方法和内联函数实现的,这些操作符都是为了方便代码开发而添加的,随着kotlin越来越成熟,方便开发人员使用的操作符也会越来越多,单纯靠总结现有的操作符是无法全部掌握的,如果遇到不了解的操作符,可以进入操作符的方法中,查看一下源码的实现方式,再配合注释就可以轻松使用大部分操作符了。

参考文章

Kotlin系列之let、with、run、apply、also函数的使用@熊喵先生

结尾


最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容