前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >kotlin修炼指南8—集合中的高阶函数

kotlin修炼指南8—集合中的高阶函数

作者头像
用户1907613
发布2022-12-12 12:15:39
4480
发布2022-12-12 12:15:39
举报
文章被收录于专栏:Android群英传Android群英传

Kotlin对集合操作类新增了很多快捷的高阶函数操作,各种操作符让很多开发者傻傻分不清,特别是看一些Kotlin的源码或者是协程的源码,各种眼花缭乱的操作符,让代码完全读不下去,所以,本文将对Kotlin中的集合高阶函数,进行下讲解,降低大家阅读源码的难度,下面看几个用的比较多的高阶函数使用。

首先是sumOf,作为一个很方便的求和函数,它可以快速对集合内的某些参数进行sum操作,代码如下所示。

代码语言:javascript
复制
val list = mutableListOf(1, 2, 3, 4)
val sumOf = list.sumOf { it }

我们来看看它的源码。

代码语言:javascript
复制
public inline fun <T> Iterable<T>.sumOf(selector: (T) -> Int): Int {
    var sum: Int = 0.toInt()
    for (element in this) {
        sum += selector(element)
    }
    return sum
}

其实它内部就是对元素的累加,像这样的高阶函数,在Kotlin中有很多,这也是很多基础功能用Kotlin开发会更加方便的原因之一。

但是sumOf有个局限,那就是只能求和,毕竟它设计就是用来作求和的,所以对于更加一般的场景,我们可以将这个操作再进一步抽象出来,这就是reduce。

比如我们现在要实现一个乘法功能,代码如下所示。

代码语言:javascript
复制
val list = mutableListOf(1, 2, 3, 4)
val result = list.reduce { acc, i ->
    acc * i
}

reduce的操作参数有两个,当前的累积值和集合中的下一个元素。reduce的执行逻辑是,先取出集合的第一个元素,作为acc,并和第二个元素——i,执行block中的逻辑,返回值作为acc,继续上面的步骤。

❝如果集合为空,那么会导致异常。 ❞

但是reduce也有个局限问题,那就是它默认使用集合的第一个元素作为起始的acc,所以它就只能返回前面集合的泛型类型,假如是下面这样的结构,就无法使用了。

代码语言:javascript
复制
data class Test(val num: Int, val name: String)
val list = mutableListOf(
    Test(1, "x"),
    Test(2, "y"),
    Test(3, "z"),
    Test(4, "j")
)
val result = list.reduce { acc, i ->
    acc.num * i.num // Error
}

其问题,就是在于acc的类型不能指定,只能从集合中获取,所以,Kotlin还提供了更加通用的高阶函数——fold,代码如下所示。

代码语言:javascript
复制
data class Test(val num: Int, val name: String)
val list = mutableListOf(
    Test(1, "x"),
    Test(2, "y"),
    Test(3, "z"),
    Test(4, "j")
)
val result = list.fold(1) { acc, test ->
    acc * test.num
}

fold和reduce非常像,只不过fold增加了一个initial的参数,通过这个参数,可以设置acc的初始值,同时也指定了返回的类型,这样一来,就不像reduce一样需要和集合类型保持一致了。

❝由于初始值是initial参数指定的,所以即使集合为空也不会导致异常。 ❞

由此可见,在Kotlin中,reduce实际上是一个不完善的高阶函数,大部分时候,都应该避免使用它,而应该使用flod来代替,而且,要注意的是,在其它语言中,例如JavaScript中,它的reduce函数,实际上和Kotlin的fold函数的逻辑是一样的,而不是Kotlin中reduce的实现。

那么fold有什么使用场景呢?前面说的对集合进行遍历,然后对某些项目进行求和、求积、拼接字符串这些操作,就是一个非常常用的例子。

❝和大部分的集合高阶函数一样,fold也提供了foldRight、foldIndexed、foldRightIndexed这样的拓展,可以通过获取索引,或者是改变遍历的方向。 ❞

fold和reduce,实际上是一种对集合的规约操作,最后会返回一个「规约」之后的值,相当于对集合做提取并规约的操作。

除了对集合的规约,对集合的遍历,Kotlin也做了很多改善。

例如我们可以通过filter来过滤集合中满足某些规则的元素,代码如下所示。

代码语言:javascript
复制
val result = list.filter { it.num > 2 }

再例如对集合做排序,虽然之前也能做,但是绝对不像高阶函数这样一目了然,让我们看下下面的代码。

代码语言:javascript
复制
val sorted = lists.sorted()
val sortedDescending = lists.sortedDescending()
val sortedBy = lists.sortedBy { it.length }
val sortedByDescending = lists.sortedByDescending { it.length }
val sortedWith = lists.sortedWith(
    compareBy<String> { it.length }.thenBy { it }
)

这些都是常见的排序方法,基本可以涵盖我们大部分的使用场景。

除了排序,我们还可以对集合做Check,判断集合中是否有满足条件的元素,例如下面的代码。

代码语言:javascript
复制
val any = lists.any { it.length == 6 }
val all = lists.all { it.length == 6 }
val none = lists.none { it.length == 6 }
val count = lists.count { it.length == 6 }

类似的例如Search和take这样的高阶函数我们就不讲了,基本都可以望文生义。

最后我们来看下集合中的Transform。

最简单的,我们可以借助map函数来对一个集合做转换,例如下面的代码。

代码语言:javascript
复制
val result = list.map { it.num }

这样就形成了一个num组成的新集合。

map相对来说比较好理解,它实现的是一对一的转换,但是另一个——flatMap就不是这么好理解了。

所以我们先来了解另一个操作符——flatten。

假设我们有这样一个嵌套的List,如下所示。

代码语言:javascript
复制
val list = listOf(listOf("abc", "xyz", "hjk"), listOf("123", "789"), listOf("+-"))

我需要将这个二维List打平为一个一维List,那么就可以通过flatten来实现,代码如下所示。

代码语言:javascript
复制
val result = list.flatten()
// out
[abc, xyz, hjk, 123, 789, +-]

那么如果我在打平List之后,还要对数据做一些处理呢?很方便,我们可以链式调用其它的高阶函数,例如map,代码如下所示。

代码语言:javascript
复制
val result = list.flatten().map { it.first() }
// out
[a, x, h, 1, 7, +]

这样的操作其实很常见,所以Kotlin提供了一个复合的高阶函数——flatMap,我们使用flatMap来实现同样的功能。

代码语言:javascript
复制
val result = list.flatMap {
    it.map { item ->
        item.first()
    }
}

它实际上就是先使用flatten将数据打平,再对每个item进行map操作。所以,如果你只是需要打平数据,那么直接flatten就够了,如果需要再对数据做一些处理,那么就需要使用flatMap了。

flatMap的一个非常常用的场景,就是生成两个List的叉乘数据,我们来看下面这个例子。

代码语言:javascript
复制
val SUITS = setOf("♣" /* clubs*/, "♦" /* diamonds*/, "♥" /* hearts*/, "♠" /*spades*/)
val VALUES = setOf("2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K", "A")

上面有两个list,分别是扑克牌中的花色和数字,那么我们如何通过这两个list来生成整副扑克牌呢?借助flatMap就可以很方便的实现,代码如下所示。

代码语言:javascript
复制
val DECK = SUITS.flatMap { suit ->
    VALUES.map { value -> Card(suit, value) }
}

这个例子和上面的例子还不太一样,可以说是一个互逆的过程,前面我们是通过一个嵌套List,然后打平处理数据,而这个例子,则是两个list进行叉乘,生成一个新的List。

综上,我们总结下flatMap的工作流程,首先,flatMap会遍历集合中的元素,然后将每个元素传入block中,经过block处理后返回一个list,最后将每个元素处理完后生成的list进行平铺,生成一个打平的list,这就是flatMap的完整执行流程。

由此可见,大部分场景下,我们甚至都不用再使用集合的遍历功能,通过这些辅助的高阶函数,就可以很方便的对集合进行操作,这也是Kotlin代码会比Java更加容易开发的原因,当然,Kotlin的函数式编程方式,会比Java的上手难度更高。

那么我们在使用Kotlin的高阶函数来对集合进行处理时,是否需要担心一些隐藏的性能开销呢?

首先,Kotlin默认的集合类高阶函数,都是inline函数,所以在编译时会进行替换,从而高阶函数的block不会生成新的内部类,造成代码膨胀,但是,由于高阶函数每次处理集合时,都会产生一个新的集合,所以确实会造成内存的增长,但是对于移动端来说,在数据量不大的场景下,这个影响是微乎其微的,所以,完全不用担心性能的开销,放心大胆的使用吧。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2022-11-07,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 群英传 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档