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

2022-12-12 12:15:39 浏览数 (2)

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不会生成新的内部类,造成代码膨胀,但是,由于高阶函数每次处理集合时,都会产生一个新的集合,所以确实会造成内存的增长,但是对于移动端来说,在数据量不大的场景下,这个影响是微乎其微的,所以,完全不用担心性能的开销,放心大胆的使用吧。

0 人点赞