Kotlin高阶函数、内联函数以及集合变换序列

一、高阶函数

高阶函数的一个重要特征就是参数类型包含函数,或者该函数的返回值类型是一个函数类型,那么该函数就被称为是高阶函数。

下面看看如何把函数作为参数声明到另一个函数中:

//参数包含函数类型,()代表一个匿名函数,Unit代表无返回值
fun paramFunction(block:() -> Unit){
    block()
}
复制代码

返回值是是一个函数的形式

fun returnFunction():() -> Long{
    return {System.currentTimeMills()}
}
复制代码

高阶函数的调用

高阶函数的一个重要特征就是可以让函数类型参数决定函数的执行逻辑,就是说相同返回类型但执行逻辑不同的函数作为参数传入时,返回结果是不一样的。下面我们定义一个函数看一下:

//定义一个所谓的高级函数 函数参数block是两个参数为Int类型的函数变量,返回值是Int
fun exampleFun(a: Int, b: Int, block: (Int, Int) -> Int): Int {
    return block(a, b)
}

//定义两个返回类型一样 但执行逻辑不一样的函数
fun aFun(n:Int,m:Int):Int{
    return m*n
}

fun bFun(n:Int,m:Int):Int{
    return m+n
}

//调用
fun main() {
    val aValue = exampleFun(2,3,::aFun)
    println("aFun作为参数的返回值:$aValue")
    val bValue = exampleFun(2,3,::bFun)
    println("bFun作为参数的返回值:$bValue")
}

//输出结果
aFun作为参数的返回:6
bFun作为参数的返回:5

复制代码

这里有一点就是我们需要定义多个逻辑不同的函数,相对来说有点麻烦,但是其实我们可以看到,传的函数参数是可以简化成lamba表达式,之前我们学lamba表达式的可以知道:

val func:(Int) -> Unit = {p:Int -> println(p) }

那上述的block对应的lamba表达式就是:

val block:(Int,Int) -> Int = {a:Int,b:Int -> a+b }

其中a+b就是执行逻辑,可以是a+b,a-b或者a*b,只要返回Int类型就行。

那前面的高级函数调用可以灵活写成

val aValue = exampleFun(2,3){a,b -> a+b}
复制代码

二、内联函数

内联函数就是在函数前面加上 inline关键字修饰。内联函数的使用可以减少函数的调用,有性能开销的优化作用,这里高阶函数和和内联会更匹配,原因在于调用高阶函数的时候,作为函数参数的变量调用也会是一个函数的调用,这个时候就会创建一个匿名类对象代替Lamba表达式,对象创建越多,内存开销肯定会大。看看下面这段代码

    fun inlineFun(block:() -> Unit){
        val curTime = System.currentTimeMillis()
        block()
        println(System.currentTimeMillis() - curTime)
    }
    
    //inlineFun的lamba表达式方式写法
    inlineFun{
        println("Kotlin")
    }
    
    //这个时候,我们只是想调用打印这个方法,看看它调用的时间,但这个时候依旧会创建lamba表达式的匿名类,
    //这样消耗的时间可能会大于打印时间
    
   //那我们用定义为内联函数后的调用是如何的
   
   inline fun inlineFun(block:() -> Unit){
        val curTime = System.currentTimeMillis()
        block()
        println(System.currentTimeMillis() - curTime)
   }
    
   //这个时候再调用
   inlineFun{
       println("Kotlin")
   }
   
   //其实际执行的是
   val curTime = System.currentTimeMillis()
   println("Kotlin")
   println(System.currentTimeMillis() - curTime)
复制代码

这样编译时创建lamba表达式匿名类开销就没了,所以内联函数的原理是:Kotlin在编译时会把内联函数内代码搬到要调用的地方, 就好像我们写了这段代码一样。

内联函数的noinline和crossinline

一个高阶函数如果被inline关键字修饰,那么它接收的所有函数类型的参数均会被内联,如果想某个函数类型的参数不被内联,就需要用关键字noinline修饰。

那这就有一个问题:既然前面我们说内联函数能消除Lambda表达式运行时带来的额外内存开销,那么为啥还提供了一个noinline来排除内联呢?

这里主要是因为内联函数类型的参数只能传递给内联函数,而非内联则可以传递给任何函数,另外不同的一点是内联函数可以返回外部函数,就是调用该内联函数的函数,而非内联函数则返回局部。

下面代码看看内联函数的返回和非内联函数的返回:

inline fun nonLocalReturn(block:() -> Unit){
    block()
}

fun main(){

    println("main start")
    nonLocalReturn{
        println("lambda start")
        //这里return 就是直接返回到main函数了
        return
        println("lambda end")
    }
    println("main end")
}
//执行打印的结果是
main start
lambda start
说明直接返回到main函数了,没有再往下执行


//如果我们没有定义成内联函数
fun nonLocalReturn(block:() -> Unit){
    block()
}

fun main(){
    println("main start")
    nonLocalReturn{
        println("lambda start")
        return@nonLocalReturn
        println("lambda end")
    }
    println("main end")
}

//这里打印的结果是
main start
lambda start
main end
说明仅仅是返回了nonLocalReturn函数,main函数还是往下走了
复制代码

crossinline关键字

绝大多数高阶函数都可以声明为内联函数,但是也有例外的情况。如下列代码:

inline fun Runnable(block:() -> Unit):Runnable{
    return object:Runnbale {
        override fun run(){
            block()
        }
    }
}
复制代码

这个时候idea会提示block()调用错误,这是因为有可能存在不合法的non-local return,block()的调用处与定义处不在一个调用上下文。

那么如何解决这个问呢?

使用crossinline关键字修饰传递的函数就可以使它在Lambda表达式中一定不return,而且crossinline修饰除了不return之外,其他内联函数的属性都是一样的。

三、几个常用的高阶函数

let函数

let函数调用一般是针对一个定义在特定情况下使用的变量,例如我们可以避免对变量的null判断。观察如下代码:

// any 不为null 时才会调用let 函数
any?.let {
    //it 就是代表any对象
    // todo()方法就是any对象的方法
    // it.todo()的返回值作为let函数的返回值返回
    it.todo() 
}
复制代码

在Android开发中的实际场景就是我们可能要对一个变量进行多次判空才调用,如:

mTextView?.text = "Kotlin"
mTextView?.textSize = 16f
复制代码

使用let函数后可以简写为

mTextView?.let{
    it.text = "Kotlin"
    it.textSize = 16f
}
复制代码

run函数

run函数其实就是let函数的升级版,run函数接受一个lambda 函数为参数,传入this并以闭包形式返回,返回值是最后的计算结果,那么上述的代码可以优化成下面这样:

mTextView?.run{
    text = "Kotlin"
    textSize = 16f
}
复制代码

apply函数

apply函数和run函数的结构模式很相像,但是apply函数返回的是调用对象本身,因为apply函数的这个特性,所以它特别有助于我们进行多级的判null行为。下面我们通过一个代码示例了解一下:

apply函数的一般结构:

val applyRes = any.apply{
    //todo()是 any 对象的共有属性或方法
    todo()
    // 最后返回的是any对象,而不是2
    1+1
}
复制代码

apply的简单应用举例:

//定义了一个公司对象 包含部门和人员名字
class Company(var department:Department? = null){
    class Department(var departmentName: String? = null,var person:Person? = null){
        class Person(var name:String? = null)
    }
}


fun main() {
    val company:Company? = Company(Company.Department("一部",  Company.Department.Person("Jeremy")))
    company?.department?.apply {
        departmentName = "二部"
    }?.person?.name.also {
        println("这个${company?.department?.departmentName}的员工是$it")
    }
}

打印的信息是:这个二部的员工是Jeremy
复制代码

also函数

上述的apply应用举例中,我们最后调用了also函数,这个函数的作用和let函数是类似的,其中it就是返回的调用对象本身,其一般的结构格式就是:

val alsoRes = any.also {
    //it代表的就是any todo是any的属性方法
    it.todo() 
    1+1 //返回的是any对象,而不是2
}
复制代码

use函数

use函数最大的一个特征就是会自动关闭调用者,无论其中间是否出现异常,因为use函数内部实现也是通过try-catch-finally块捕捉的方式,而close操作在finally里面执行,所以无论是正常结束还是出现异常,都能正确关闭调用者。

怎么才能调用use函数呢?

凡是实现了Closeable接口的对象都可以调用use函数。

因为use函数使得Kotlin中对File对象和IO流操作变得行云流水。

File("build.gradle").inputStream().reader().buffered()
    .use {
        print(it.readLines())
    }
复制代码

四、集合变换序列

4.1、filter操作

filter的操作是保留满足条件的元素得到新的列表,观察下列代码:

val arr = intArrayOf(1,2,3,4)
//asSequence()转换成懒序列
val arrFilter = arr.asSequence().filter { it%2 == 0 }
val arrFilter = arr.filter { it%2 == 0 }

for (e in arrFilter){
    println(e)
}

打印的数据:
2
4
复制代码

4.2、map变换

map变换就是一种映射一一对应的操作:

val list: MutableList<Int> = mutableListOf(1, 2, 3, 4)
val listMap = list.map { it*2 + 1}
for (e in listMap){
    println(e)
}

打印的数据是:
3
5
7
9

集合的每个数据对应执行lamba表达式中的it*2 + 1返回一个新的数据集合
复制代码

下面我们看一下filter和map结合的使用

懒序列调用法:

val list: MutableList<Int> = mutableListOf(1, 2, 3, 4)
list.asSequence().filter {
    println("filter $it")
    it % 2 ==0
}.map {
    println("map $it")
    it*2 + 1
}.forEach {
    println("forEach $it")
}

打印结果:
filter 1
filter 2
map 2
forEach 5
filter 3
filter 4
map 4
forEach 9
复制代码

上述看到打印结果是主要有符合条件的都会先往下走,懒序列有一个很重要的点就是forEach是一个阀门作用,如果把forEach去掉就不会执行前面filter和map的操作,这就是懒序列的意义,当你需要的时候才会执行。那我们看一下饿汉式的调用结果:

val list: MutableList<Int> = mutableListOf(1, 2, 3, 4)
list.filter {
    println("filter $it")
    it % 2 ==0
}.map {
    println("map $it")
    it*2 + 1
}.forEach {
    println("forEach $it")
}

打印结果:
filter 1
filter 2
filter 3
filter 4
map 2
map 4
forEach 5
forEach 9
复制代码

可以看到它无论是否符合条件,都会先执行完再进行下一步操作。

4.3、flatMap变换

flatMap就是把集合的元素映射成集合,每个元素对应一个新的集合,最后把这些集合组成一个最终的新的集合。如下面代码:

val list: MutableList<Int> = mutableListOf(1, 2, 3, 4)
list.flatMap { 0 until it }.joinToString().let(::println)

打印结果
0, 0, 1, 0, 1, 2, 0, 1, 2, 3
即是每个元素依次对应的集合是:
0 -> 0
1 -> 0,1
2 -> 0,1,2
3 -> 0,1,2,3
复制代码

flatMap的返回值只有是Iterable即可,如下:

  val list: MutableList<Int> = mutableListOf(1, 2, 3, 4)
  val listFlatMap = list.flatMap { listOf(it+1) }
  for (e in listFlatMap){
     println(e)
  }
  
 打印结果:
 2
 3
 4
 5
复制代码

4.4、集合的聚合操作举例

sum:所有元素求和。

reduce:将元素依次按规则聚合,结果与元素类型一致。

fold:给定初始化值,将元素按规则聚合,结果与初始化值类型一致。

其中reduce其实就是fold的简化版,我们简单看一下fold函数的操作:

val list: MutableList<Int> = mutableListOf(1, 2, 3, 4)
val strFold = list.fold(StringBuffer()){
    //acc就是上次拼接的结果
    acc, i -> acc.append(i)
}
println(strFold)

打印结果:
1234
复制代码

Guess you like

Origin juejin.im/post/7066484113628200997