Kotlin的高阶函数和常用高阶函数

高阶函数的定义

将函数当做参数或者是返回值的函数

什么是高阶函数

可以看看我们常用的 forEach 函数:

1public inline fun <T> Array<out T>.forEach(action: (T) -> Unit): Unit {
2    for (element in this) action(element)
3}

首先我们可以知道, forEachArray 的扩展函数,然后参数是 action ,但是 action 不再像和我们以前Java那样传递的是一个对象,这时传递的是一个函数 。这个函数的入参为 T ,返回值为 Unit 。所以 forEach 也是一个高阶函数,因为它将函数当做参数进行传递了。我们尝试着去调用一下 forEach 函数:

1fun main(args: Array<String>) {
2    args.forEach(::println)
3}

调用的时候,我们将 println 函数传递给了 forEach 函数,这里采用的是函数引用 。就上诉代码,我们还可以结合 Lambda 表达式来进行处理:

 1fun main(args: Array<String>) {
 2    args.forEach ({
 3        println(it)
 4    })
 5    args.forEach{
 6        println(it)
 7    }
 8    args.forEach(){
 9        println(it)
10    }
11}

其实以上几种的方式得到的结果都是一样的,但是第一种就是简洁了许多。

我们再定义一个类,用来打印 forEach 的值:

1class PdfPrinter {
2    fun println(any: Any) {
3        kotlin.io.println(any)
4    }
5}

根据函数引用的特性,我们可以这样调用 println(any: Any) 函数:PdfPrinter::println 。由于 PdfPrinter 中的 println 函数的入参类型是 Any 类型,也就是任意类型,不管 forEach 传递的是什么值都可以接收。那现在我们再将其作为 forEach 的参数传递进去:

编译器告诉我们这个是错误的。那我们来分析一下吧:我们再定义一个 Hello

1class Hello {
2    fun world() {
3        println("Hello World.")
4    }
5}

然后进行以下操作:

可以看到 Android Studio 分别对 helloWorldprinter 的解释:

  • helloWorld 是一个方法,然后参数类型为 Hello ,返回值为 Unit
  • printer 也是一个方法,但是参数有两个,分别是 PdfPrinterAny 类型, 返回值为 Unit

forEach 中,只有一个参数传递,但是 PdfPrinter::println 需要的是两个参数,肯定就会报错,所以我们需要对此进行修改:

1fun main(args: Array<String>) {
2    var pdfPrinter = PdfPrinter()
3    args.forEach(pdfPrinter::println)
4}

这个样子就OK了。

常用高阶函数

map:变换

通常我们会使用以下的方式来实现对集合中的元素进行修改的操作:

 1fun main(args: Array<String>) {
 2    var list = listOf(1, 3, 4, 7, 89)
 3    var newList = mutableListOf<Int>()
 4    list.forEach {
 5        var element = it * 2 + 1
 6        newList.add(element)
 7    }
 8    newList.forEach(::println)
 9}
10打印结果:
113
127
139
1415
15179

如果采用这种方式,远远不能体现Kotlin的优势了,这个和Java有什么区别呢?「狗子,上map」:

 1fun main(args: Array<String>) {
 2    var list = listOf(1, 3, 4, 7, 89)
 3    var newList = list.map {
 4        it * 2 + 1
 5    }
 6    newList.forEach(::println)
 7}
 8打印结果:
 93
107
119
1215
13179

从打印结果可以看到他们的实现效果是一模一样的,这个就是 map 的功能,可以对集合中的元素进行你想要的操作,是不是跟 RxJavamap 很类似呢!我们来细看一下map 的实现原理:

 1@Suppress("NON_PUBLIC_CALL_FROM_PUBLIC_INLINE")
 2public inline fun <T, R> Iterable<T>.map(transform: (T) -> R): List<R> {
 3    // 新创建一个ArrayList,默认长度是10,将ArrayList跟transform传递给mapTo
 4    return mapTo(ArrayList<R>(collectionSizeOrDefault(10)), transform)
 5}
 6
 7public inline fun <T, R, C : MutableCollection<in R>> Iterable<T>.mapTo(destination: C, transform: (T) -> R): C {
 8    // 进行迭代遍历
 9    for (item in this)
10    // 将进行过变化的数据添加到新的集合中
11        destination.add(transform(item))
12    return destination
13}

map 方法中主要做的就是调用 mapTo 方法,然后传递的是新创建并且初始长度为10的 ArrayListtransform 函数,在 mapTo 方法中,对集合进行迭代,然后将进行变换后的数据添加到新的集合中,最后返回新的集合。

map 操作不仅可以将元素变换成与之前类型相同的元素,也可以变化成与之前元素类型不同的元素,具体你想变换成什么类型,这个是不做限制的。

 1fun main(args: Array<String>) {
 2    var list = listOf(1, 3, 4, 7, 89)
 3    var newList = list.map(Int::toDouble)
 4    newList.forEach(::println)
 5}
 6打印结果
 71.0
 83.0
 94.0
107.0
1189.0

flatMap:变换

 1fun main(args: Array<String>) {
 2    var list = listOf(1..20, 3..50, 4..100)
 3    var newList = list.flatMap {
 4        it
 5    }
 6    newList.forEach(::println)
 7}
 8打印结果
 91
102
11。。。
12100

flatMap 看起来跟 map 很相似,其实真的很类似,搞得有时候自己都不知道应该使用哪个操作符了,那就从源码来看看它们之间的区别吧。

 1public inline fun <T, R> Iterable<T>.flatMap(transform: (T) -> Iterable<R>): List<R> {
 2    // 新创建一个ArrayList,将ArrayList跟transform传递给mapTo
 3    return flatMapTo(ArrayList<R>(), transform)
 4}
 5
 6public inline fun <T, R, C : MutableCollection<in R>> Iterable<T>.flatMapTo(destination: C, transform: (T) -> Iterable<R>): C {
 7    for (element in this) {
 8        // 执行过transform以后的结果还是一个list集合,这里就是map和flatMap之间的区别了
 9        val list = transform(element)
10        // 将变换后的集合整个添加到新的集合中
11        destination.addAll(list)
12    }
13    return destination
14}

可以看到 flatmap 中的参数 transform 是一个返回值为 Iterable<R> 的函数,而 map 的参数 transform 是一个返回值为 R 的函数。然后调用 flatMapTo 方法,将 transform 和一个新创建的 ArrayList 传递给了 flatMapTo 方法。在 flatMapTo 方法中,对当前的集合进行了迭代,然后将执行过变换操作后的集合数据全部添加到新的集合中,最终返回新的集合。

mapflatMap 的主要区别就是在于传入的函数的返回值,一个是任意对象,一个是实现了 Iterable 接口的对象

reduce

例子:打印集合中的元素之和

 1fun main(args: Array<String>) {
 2    var listOf = listOf(1, 2, 3, 4, 5, 6, 7)
 3    var reduce = listOf.reduce { acc, i ->
 4        println("$acc   $i")
 5        acc + i
 6    }
 7    println(reduce)
 8}
 9打印结果:
101   2
113   3
126   4
1310   5
1415   6
1521   7
1628

还是直接对源码进行分析吧,感觉看了源码就一目了然了。

 1public inline fun <S, T: S> Iterable<T>.reduce(operation: (acc: S, T) -> S): S {
 2    val iterator = this.iterator()
 3    // 如果当前的集合为空就会抛出异常
 4    if (!iterator.hasNext()) throw UnsupportedOperationException("Empty collection can't be reduced.")
 5    // 首先将集合中的第一个元素赋值给accumulator
 6    var accumulator: S = iterator.next()
 7    while (iterator.hasNext()) {
 8        // 执行operation,将accumulator和下一个元素传递给operation函数(也就是传递进行来的函数)
 9        // 然后将返回结果赋值给accumulator
10        accumulator = operation(accumulator, iterator.next())
11    }
12    return accumulator
13}

首先对当前的集合进行判空处理,接着将第一个元素赋值给 accumulatoraccumulator 的类型是 S 。然后对当前集合进行迭代处理,并调用我们传递进去的参数 operationoperation 函数中传递了两个参数,一个是 S 类型的,一个是集合元素类型的。operation 函数的返回值也是 S 类型的,将 operation 的返回值重新赋值给 accumulator 。迭代完毕以后返回我们的 accumulator

其实通过我们解读源码以后,我们就可以知道 reduce 函数会将上一次的计算结果传递到下一次计算中,我们可以利用这个方式来实现以下字符串拼接,当然我们的字符串拼接有其他更好的方式,这里只是做为讲解 reduce 的例子而已:

 1fun main(args: Array<String>) {
 2    var listOf = listOf("H", "e", "l", "l", "o", "W", "o", "r", "l", "d")
 3    var reduce = listOf.reduce { acc, i ->
 4        println("$acc   $i")
 5        "$acc$i"
 6    }
 7    println(reduce)
 8}
 9打印结果:
10H   e
11He   l
12Hel   l
13Hell   o
14Hello   W
15HelloW   o
16HelloWo   r
17HelloWor   l
18HelloWorl   d
19HelloWorld

fold:能够添加初始值的reduce

不得不说,foldreduce 的作用基本是一致的,只是 fold 能够添加初始值,什么叫做能够添加初始值呢?让我们来举个栗子看看呗!

 1fun main(args: Array<String>) {
 2    var listOf = listOf("H", "e", "l", "l", "o", "W", "o", "r", "l", "d")
 3    var fold = listOf.fold("老铁说:") { acc, i ->
 4        println("$acc   $i")
 5        "$acc$i"
 6    }
 7    println(fold)
 8}
 9打印结果:
10老铁说:   H
11老铁说:H   e
12老铁说:He   l
13老铁说:Hel   l
14老铁说:Hell   o
15老铁说:Hello   W
16老铁说:HelloW   o
17老铁说:HelloWo   r
18老铁说:HelloWor   l
19老铁说:HelloWorl   d
20老铁说:HelloWorld

还是看源码吧:

1public inline fun <T, R> Iterable<T>.fold(initial: R, operation: (acc: R, T) -> R): R {
2    // 将初始值赋值给 accumulator 
3    var accumulator = initial
4    // 集合进行遍历操作,将accumulator和遍历到数据传递给 operation 函数,执行operation以后,
5    // 将operation函数的返回值有赋值给了accumulator,接着下一步的遍历
6    for (element in this) accumulator = operation(accumulator, element)
7    return accumulator
8}

看着源码就会觉得这些函数的操作很是简单了。fold 函数还有很多的兄弟:

  • foldRight 说的再多也不如看结果 1fun main(args: Array<String>) { 2 var listOf = listOf("H", "e", "l", "l", "o", "W", "o", "r", "l", "d") 3 // 注意!!!注意!!!注意!!!这边的参数跟fold函数调用的参数位置是相反的,具体原因可以看源码 4 var reduce = listOf.foldRight("老铁说:") { i, acc -> 5 println("$acc $i") 6 "$acc$i" 7 } 8 println(reduce) 9 10 val add5 = add(5) 11 println(add5(2)) 12} 13打印结果: 14老铁说: d 15老铁说:d l 16老铁说:dl r 17老铁说:dlr o 18老铁说:dlro W 19老铁说:dlroW o 20老铁说:dlroWo l 21老铁说:dlroWol l 22老铁说:dlroWoll e 23老铁说:dlroWolle H 24老铁说:dlroWolleH
  • foldRightIndexed 这个函数就是多了一个 index 的参数,具体的用处暂时没有发现,就不做数据打印了。

filter:过滤

例子:过滤集合中的奇数

 1fun main(args: Array<String>) {
 2    var listOf = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9)
 3    var filter = listOf.filter { it % 2 == 0 }
 4    filter.forEach(::println)
 5}
 6打印结果:
 72
 84
 96
108

上源码:

 1public inline fun <T> Iterable<T>.filter(predicate: (T) -> Boolean): List<T> {
 2    // 新创建一个ArrayList,将ArrayList和predicate函数一起传递给filterTo
 3    return filterTo(ArrayList<T>(), predicate)
 4}
 5
 6public inline fun <T, C : MutableCollection<in T>> Iterable<T>.filterTo(destination: C, predicate: (T) -> Boolean): C {
 7    // 为了保证代码跟源码的样式一致,这里就不做对代码的样式修改
 8    // 对当前的数组进行遍历,如果满足predicate(element)条件,就将当前元素加入到新的集合中
 9    for (element in this) if (predicate(element)) destination.add(element)
10    // 将新的集合返回
11    return destination
12}

filter 中创建新的集合 ArrayList ,将 ArrayListpredicate 函数一并传递给 filterTo 函数。在 filterTo 函数中,先对当前的集合进行遍历,如果满足条件 predicate(element) 就将当前的元素添加到新的集合中, predicate(element) 就是我们传递进来的那个函数,返回值是一个 Boolean 类型的。

takeWhile:截取集合中的数据,直到第一个不满足条件的元素为止

例子:截取集合中不能够被5整除的数,直到第一个不满足条件的元素为止。

 1fun main(args: Array<String>) {
 2    var listOf = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9)
 3    var takeWhile = listOf.takeWhile { it % 5 != 0 }
 4    takeWhile.forEach(::println)
 5}
 6打印结果:
 71
 82
 93
104

源码:

 1public inline fun <T> Iterable<T>.takeWhile(predicate: (T) -> Boolean): List<T> {
 2    // 新创建一个集合ArrayList
 3    val list = ArrayList<T>()
 4    // 遍历当前集合
 5    for (item in this) {
 6        if (!predicate(item))
 7            // 如果不满足条件结束遍历,也不会将当前不满足条件的元素添加到新的集合中
 8            break
 9        // 将满足条件的元素添加到集合中
10        list.add(item)
11    }
12    // 返回新创建的集合
13    return list
14}

let:将调用者当做参数传递给指定的函数

例子:省略if空判断

 1fun main(args: Array<String>) {
 2    // 获取一个Person对象,这个对象是可空的
 3    var person = getPerson()
 4    // 如果person为空,将不会调用let函数,也不会去执行后面的代码
 5    // 如果不使用let函数的话,只能采取以下写法:
 6    //    if (person != null) {
 7    //        printPerson(person)
 8    //        println(person.name)
 9    //        println(person.age)
10    //    }
11    // 当然这个例子真的只是例子,暂时还是不能区别出使用let方式的优势,只有用到复杂的逻辑实战中才能体现出
12    person?.let {
13        printPerson(person)
14        println(person.name)
15        println(person.age)
16    }
17}
18
19fun printPerson(person: Person){
20     // 操作person   
21}
22
23fun getPerson(): Person? {
24    return null
25}
26
27class Person(var name: String, var age: Int) {
28}

源码:

1@kotlin.internal.InlineOnly
2public inline fun <T, R> T.let(block: (T) -> R): R = block(this)

源码也是很简单,就是将调用者传递给传入进来的函数并执行传入进来的函数。

apply:执行指定函数,并且返回调用者

例子:修改person类的age属性

1fun main(args: Array<String>) {
2    var person = Person("laotie",18)
3    var apply = person?.apply {
4        this.age = 16
5    }
6    println(apply)
7}
8data class Person(var name: String, var age: Int) 

上诉的例子真的只是例子,它没有跟你讲 apply 有多强大,它只是描述了的作用。源码:

1@kotlin.internal.InlineOnly
2public inline fun <T> T.apply(block: T.() -> Unit): T { block(); return this }

就是单纯的执行函数 block 并返回调用者。

with

例子:文件读取

 1fun main(args: Array<String>) {
 2    var reader = BufferedReader(FileReader("build.gradle"))
 3    with(reader){
 4        var line:String?
 5        while (true){
 6            // this 对象就是reader
 7            line = this.readLine()?:break
 8            println(line)
 9        }
10    }
11}
12// 结果就不打印了

源码:

1@kotlin.internal.InlineOnly
2public inline fun <T, R> with(receiver: T, block: T.() -> R): R = receiver.block()

with 接收两个参数,一个是 receiver ,上诉例子中就是 reader ,另一个就是 函数 ,在上诉例子中,我们使用了 Lambda 表达式,所以这个函数就移到了括号外面了。

疑惑点

函数定义中 `T.() -> Unit` 和 `() -> Unit` 的区别

我们一般定义函数都会选择定义

1fun <T> T.makeMoney2(block: () -> Unit) {
2    block()
3}

上诉代码表示:T 的扩展方法 makeMoney 接收一个 block 的函数,该函数是无参无返回的。

那我们再见识见识 T.() -> Unit 这种方式定义的方法,其实也很多见了:

1public inline fun <T> T.apply(block: T.() -> Unit): T { block(); return this }

可以看到 apply 函数使用的是 T.() -> Unit 这种方式,他们两到底有啥区别呢?

我在 ScrollingActivity 定义了两个方法:

1fun <T> T.makeMoney1(block: T.() -> Unit) {
2    block()
3}
4
5fun <T> T.makeMoney2(block: () -> Unit) {
6    block()
7}

分别使用 Button 调用这两个方法试试:

从图片中可以看出 :makeMoney1 中的 this 对象指的是调用对象 ,也就是 button ,而 makeMoney2 没有提示,那么我们就看打印吧:

1android.widget.Button{bb0caec VFED..C.. ......I. 0,0-0,0}
2cn.fengrong.kotlindemo.ScrollingActivity@c1cbf32

原来 makeMoney2 方法中的 this 对象指的是外部对象,在这里就是我们的ScrollingActivity 对象。

总结

这两个函数唯一的区别就是 T.()-Unit()->Unit 的区别,我们调用时,在代码块里面写this,的时候,根据代码提示,我们可以看到,连个this代表的含义不一样,T.()->Unit 里的this代表的是自身实例,而 ()->Unit 里,this代表的是外部类的实例

感谢

[Kotlin中,函数作为参数,T.()->Unit 和 ()->Unit 的区别][https://www.jianshu.com/p/88a656e59c61]

This is All

原文发布于微信公众号 - IT先森养成记(cyg_24kshign)

原文发表时间:2018-10-29

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

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏iOS技术杂谈

Java8 Lambda表达式与Stream API (一):Lambda表达式你要知道的Java8 匿名内部类、函数式接口、lambda表达式与Stream API都在这里

你要知道的Java8 匿名内部类、函数式接口、lambda表达式与Stream API都在这里 转载请注明出处 https://cloud.tencent.co...

3398
来自专栏猿人谷

标准库类型

一.标准string类型     string类型支持长度可变的字符串,C++标准库将负责管理与存储字符相关的内存,以及提供各种有用的操作。 1.1 strin...

1928
来自专栏从流域到海域

《Java程序设计基础》 第8章手记Part 2

第八章内容 Part 2 - … - 抽象类和抽象方法 - 接口及接口的实现 - 利用接口实现类的多重继承 - 内部库和匿名类 ...

2169
来自专栏小勇DW3

Java之static作用的全方位总结

 引用一位网友的话,说的非常好,如果别人问你static的作用;如果你说静态修饰 类的属性 和 类的方法 别人认为你是合格的;如果是说 可以构成 静态代码块,...

1312
来自专栏有趣的Python

6-Java基础语法-数组之一维数组

局部变量和数组的默认值问题: 局部变量是没有默认值的,如果没有初始化,是内存中的随机值。而数组是有默认值的0的,因为数组本身是对象。

1363
来自专栏程序员的知识天地

Python列表最常见的问题【总结】

列表是Python中使用最多的一种数据结果,如何高效操作列表是提高代码运行效率的关键,本文总结了一些python列表最常见的问题,希望能对大家有帮助。

1052
来自专栏WindCoder

Java基础小结(二)

继承可以使用 extends 和 implements 这两个关键字来实现继承, 而且所有的类都是继承于 java.lang.Object,当一个类没有继承的两...

991
来自专栏黑泽君的专栏

自定义异常的实现和测试以及异常的注意事项

/* * java不可能对所有的情况都考虑到,所以,在实际的开发中,我们可能需要自定义异常类。 * 而我们自己随意的写一个类,是不能作为自定义异常类来看待的...

4351
来自专栏Laoqi's Linux运维专列

文件类型+变量+数值字符串

1986
来自专栏锦小年的博客

python学习笔记7.3-内建模块collections

Python的内建模块collections为我们提供了一系列的优化操作,本质上就是元组、字典、集合、列表的特殊功能版。 1. namedtuple name...

2195

扫码关注云+社区

领取腾讯云代金券