八、kotlin的高阶函数

540 阅读7分钟


theme: Chinese-red

高阶函数

是什么?

一种以另一个函数为参数、返回值或两者兼顾的函数叫高阶函数

image.png

image.png

函数类型

整数类型, 可以存放整数, 字符串类型可以存放字符串, 而函数类型则可以存放函数引用

首先, 函数是虚拟的存在, 所以没有类型, 也没有对象 但是我们可以将函数的参数列表返回值类型归纳为一种实化类型 只要有了实化类型, 就会有对应的对象

 val sum: (Int, Int) -> Int = { x, y -> x + y }
 val action: () -> Unit = { println(42) }
 var canResultNull: (Int, Int) -> Int? = { null }
 var funOrNull: ((Int, Int) -> Int)? = null

上面(Int, Int) -> Int() -> Unit 就是函数类型

image.png

函数类型的参数名称

 fun performRequest(url: String, callback: (Int, String) -> Unit) {
     // ...
 }
 ​
 fun performRequest(url: String, callback: (code: Int, content: String) -> Unit) {
     // ...
 }

看两个区别: (1) callback: (Int, String) -> Unit (2) callback: (code: Int, content: String) -> Unit

除了参数带上名字外, 其实本质上没有区别

参数的名字只不过是为了代码的可读性

调用作为参数的函数

 fun twoAndThree(operator: (Int, Int) -> Int) {
     val result = operator(2, 3)
     println("The result is ${result}")
 }

实现一个简化版的 filter

 private fun String.myFilter02(predicate: Char.() -> Boolean): String {
    val sb = StringBuilder()
    this.forEach {
       if (predicate(it)) {
          sb.append(it)
       }
    }
    return sb.toString()
 }
 ​
 private fun String.myFilter01(predicate: (Char) -> Boolean): String {
    val sb = StringBuilder()
    this.forEach {
       if (predicate(it)) {
          sb.append(it)
       }
    }
    return sb.toString()
 }
 ​
 fun main() {
    val str: String = "abc123"
    println(str.myFilter01 { it in 'a'..'z' })
    println(str.myFilter02 { this in 'a'..'z' })
 }

函数类型默认参数

 private fun String.filter(predicate: (Char) -> Boolean = { it in 'a'..'z' }) {
     // ...
 }

返回函数的函数

根据某些条件判断返回不同的逻辑(函数引用 + 函数参数栈)

 enum class Delivery {
    STANDARD, EXPEDITED
 }
 ​
 class Order(val itemCount: Int)
 ​
 fun getShippingCostCalculator(delivery: Delivery): (Order) -> Double {
    if (delivery == Delivery.EXPEDITED) {
       return {order -> 6 + 2.1 * order.itemCount }
    }
    return { order -> 1.2 * order.itemCount }
 }
 ​
 fun main() {
    val calculator = getShippingCostCalculator(Delivery.EXPEDITED)
    println(calculator(Order(3)))
 }

返回函数保存了 函数引用 + 函数栈帧 , 看下面的代码:

 fun sum(a: Int, b: Int): Int {
    return a + b
 }
 ​
 fun myFun(a: Int, b: Int): () -> Int {
    return { sum(a, b) }
 }
 ​
 fun main() {
    // mFun 保存了函数引用和函数参数栈帧
    val mFun = myFun(20, 39)
    // 所以在这里会输出为 59, 因为它有栈帧, 保存了 a = 20, b = 39
    val i = mFun()
    println(i) // 59
 }

lambda 的使用场景之一: 去除重复代码

我们现在分析不同平台网站访问速度案例, 使用 OS 枚举来区分不同平台, 使用 data class SiteVisit 数据类, 存放访问的路径, 时间和平台

 enum class OS {
    WINDOWS, MAC, ANDROID, LINUX, IOS
 }
 ​
 data class SiteVisit(
    val path: String,
    val duration: Double,
    val os: OS
 )
 ​
 fun main() {
    val log = listOf(
       SiteVisit("/", 6.9, OS.LINUX),
       SiteVisit("/", 34.0, OS.WINDOWS),
       SiteVisit("/", 22.0, OS.MAC),
       SiteVisit("/login", 12.1, OS.WINDOWS),
       SiteVisit("signup", 8.0, OS.IOS),
       SiteVisit("/", 16.3, OS.ANDROID), SiteVisit("/", 8.2, OS.LINUX)
    )
 }

现在 需要知道 window 平台平均花费了多少时间:

 println(log.filter { it.os == OS.WINDOWS }.map(SiteVisit::duration).average())

然后需求变了, 现在需要知道 linux 平台的平均速度:

 println(log.filter { it.os == OS.LINUX }.map(SiteVisit::duration).average())

那么下次呢? 代码需要做出改变, 把一直变化的变量 OS 当作函数参数归纳出来

 fun List<SiteVisit>.averageDurationFor(os: OS) = filter { it.os == os }.map(SiteVisit::duration).average()

>>> println(log.averageDurationFor(OS.ANDROID))

然后需求又又又变了, 这次要知道移动端访问的平均速度. 上面那代码又得变:

 fun List<SiteVisit>.averageDurationFor(multiOs: Set<OS>) = filter { it.os in multiOs }.map(SiteVisit::duration).average()

>>> println(log.averageDurationFor(setOf(OS.ANDROID, OS.IOS)))

这种代码写出来一看就知道 java 1.7 用多了, 其实还有一种更自由的用法, 包括我以前也这样写, 毕竟java写这种代码没什么代价(java要用lambda就必须定义一个SAM接口, 或者选用现成的SAM接口), 而 kotlin 使用 lambda 比较方便了, 我们可以使用 lambda

 fun List<SiteVisit>.averageDurationFor(predicate: (SiteVisit) -> Boolean) = filter(predicate).map(SiteVisit::duration).average()

>>> println(log.averageDurationFor { it.os in setOf(OS.ANDROID, OS.IOS) })

内联函数, 消除 lambda 带来的代价

是什么?

内联函数, 主要的功能是一处内联, 到处(调用处)粘贴(函数体)

功能和 c语言的宏定义很像, 主要就是在调用到内联函数的位置, 直接把内联函数体拷贝过去, 然后去掉该函数的名称和作用域(也就是 {} 括号)

同时, 内联函数还可以把传入的已经调用的 lambda 参数也给内联

记住是已经 传入的并且已经调用了的lambda表达式才能内联 记住是已经 传入的并且已经调用了的lambda表达式才能内联 记住是已经 传入的并且已经调用了的lambda表达式才能内联

 inline fun f(param: () -> Unit) {
     println("inline function")
     param()
     return
 }
 ​
 fun main() {
    f { println("lambda") }
 }

反编译后:

 String var1 = "inline function";
 System.out.println(var1);
 String var4 = "lambda";
 System.out.println(var4);

传入的对象无法内联

注意只有 传入的已经被调用的 lambda 参数才可以内联, 如果传入的是一个对象则不可以内联对象, 看下面这段代码:

 fun main() {
     val v: () -> Unit = { println("lambda") }
     f(v)
 }

反编译后的java源码是这样:

 Function0 v = (Function0)null.INSTANCE;
 String var2 = "inline function";
 System.out.println(var2);
 v.invoke();

发现了没有? Function0 v = (Function0)null.INSTANCE; 这个 v 就是前面的变量, 该 lambda 表达式已经变成了对象

编译器遇到内联函数的大体处理步骤

原始代码:

 inline fun f(param: () -> Unit): Int {
    println("inline function")
    param()
    return Random(100).nextInt()
 }
 ​
 fun main() {
    println("进入 f 函数之前: ")
    val v = f { println("lambda") }
    println("v = $v")
    println("进入 f 函数之后")
 }
  1. 把函数体的lambda表达式内联掉
 inline fun f(param: () -> Unit): Int {
    println("inline function")
    println("lambda") // 内联 lambda 表达式
    return Random(100).nextInt()
 }
  1. 把函数体拷贝到调用该内联函数的位置, 把 内联函数的 return 处理下

记住是 内联函数的 return, 不是lambda的return 记住是 内联函数的 return, 不是lambda的return 记住是 内联函数的 return, 不是lambda的return

 fun main() {
    println("进入 f 函数之前: ")
    println("inline function")
    println("lambda") // 内联 lambda 表达式
    val v = Random(100).nextInt()
    println("v = $v")
    println("进入 f 函数之后")
 }

至此, 完成内联函数的内联大致过程, 其中还有在内联过程中如果遇到实参传递形参时怎么处理没搞, 会在后续讲

注意到 return 了么? 如果 inline 真的是直接拷贝内联函数体代码到调用处, 代码肯定会直接返回, 而不是 执行 println("进入 f 函数之后"), 但实际上, 它执行了.

image.png

我们把代码改成f{ println("lambda") }:

image.png

调用函数的返回值不会被内联函数直接内联到调用出, 而是去除 return 关键字, 再把 return 后面的代码拷贝过来, 如果有变量接受返回值, 就直接赋值给返回值

记住是调用函数哦, 不是被 内联函数函数类型表达式参数return 表达式

需要注意内联函数类型参数return

 inline fun f(param: () -> Unit): String {
     println("inline function")
     param()
     return "有返回值"
 }
 ​
 fun main() {
     val f = f {
         println("lambda")
         return
     }
     println(f)
 }
 inline function
 lambda

image.png

如果这段代码没有return的话

 val f = f {
     println("lambda")
 }
 println(f)

输出的将会是:

 inline function
 lambda
 有返回值

return的话:

 val f = f {
     println("lambda")
     return // 这段代码没有 return
 }
 println(f)

代码将会是下面这样:

 fun main() {
     val f = f {
         println("inline function")
         println("lambda")
         return // 这里直接返回了, 后面的 有返回值 不会再赋值给变量f
         "有返回值"
     }
     println(f) // "有返回值"
 }

所以返回的"有返回值"没有传递给变量f, 在后面的println(f)不会打印出"有返回值"这几个字

记住这和前面内联函数返回值不同, 这是内联函数参数的返回值

内联函数的返回将会被去掉return赋值给变量或者凭空执行

内联函数参数的返回值将会不经过处理, 直接原封不动的拷贝过去

为什么要引入内联函数?

答: 让内联函数中的lambda参数不再创建对象

看代码:

 fun f(param: () -> Unit) {
     println("inline function")
     param()
 }
 ​
 fun main() {
    println("进入 f 函数之前: ")
    f { println("lambda") }
    println("进入 f 函数之后")
 }

反编译后会发现 f { println("lambda") } 这段代码变成 f((Function0)null.INSTANCE); 看到没???

lambda 表达式创建了个对象, 这是一次损耗

kotlin 中 类似 f 函数这种参数有函数类型的高阶函数有很多很多, 每次传递 lambda 到这些高阶函数中都需要创建一个对象, 太损耗性能了, 所以不得不引用内联函数这项功能

前面说过, 有了内联函数, 调用内联函数的位置将被替换成内联函数的函数体, 这样就少创建了个 { println("lambda") } 对象

有什么缺点?

答: 缺点很明显, 内联函数只针对有函数类型参数的函数内联, 主要的目的为了防止 lambda 多创建的 对象, 但如果内联函数的函数体比较大, 且函数调用的位置比较多, 则会造成字节码大量膨胀, 影响 apk 包的大小

所以通常内联函数都比较短小, 特别是项目对 app 大小有严格要求的情况下, 更需要注意

高阶函数控制流

kotlin 中的非内联lambda不允许显示的使用return(它不需要, 只要返回值放在表达式末尾就是返回值), 而内联的 lambda 表达式可以显示的使用return, 但返回的是外部调用它的函数, 内联 lambdareturn会穿透一层作用域

lambda 返回最后一行的值

 fun running(a: Int, b: Int, funcType: (Int, Int) -> Int) = funcType(a, b)
 ​
 fun main() {
    running(10, 20) {x: Int, y: Int ->
       x + y
    }
 }

上面这种返回方式和下面这种一样

 fun main () {
     running(10, 20, fun(x: Int, y: Int) {
         return x + y
     })
 }

内联函数的非局部返回

看似是局部(lambda)的return, 其实是调用 lambda 函数的 return

lambda内的return会穿透一层作用域

 inline fun running(a: Int, b: Int, funcType: (Int) -> Int) {
    funcType(a + b)
 }
 ​
 fun main() {
    println("调用之前: ")
    // 下面这种方式是错误的, return it 返回的是 main 函数
    running(10, 20) {
       println(it)
       return it // 这就是 非局部返回
    }
    println("调用之后")
 }

上面的 return it 就是非局部返回, 返回的 main 函数

image.png

所以上面的提示就是让你修改 main 函数的返回值

image.png

我们可以给 return 表达式一个标签return @running it, 返回的是running函数

noinline用于未调用的lambda函数类型参数

防止照抄内联函数参数的参数名称到 main 函数中, 导致无法识别的问题

比如:

 fun main() {
     println("zhazha")
     return funcType02; // 这能不报错?
 }

它在下面是 running 函数的 参数名funcType02: (Int) -> Unit

image.png

此时我们可以添加noinline 关键字

 inline fun running(funcType01: (String) -> Unit, noinline funcType02: (Int) -> Unit) {
     funcType01("zhazha")
     return funcType02
 }
 ​
 fun main() {
     running({ println(it) }, { println(it * 10) })
 }

image.png

注意, 如果内联函数的 lambda 参数没有被调用, 最好使用使用 noinline 的话, 否则将会变成这样:

image.png

crossinline 的使用场景

主要用于 函数参数lambda 参数的lambda作为函数体内另一个函数调用时使用, 说白了就是间接调用

 fun interface MyRunnable {
    fun myF()
 }
 ​
 inline fun running(a: Int, b: Int, crossinline funcType: (Int, Int) -> Int) {
    MyRunnable { 
       // 在作用域的作用域内使用
       println(funcType(a, b))
    }.myF()
 }
 ​
 fun main() {
    running(10, 20) { x, y ->
       x + y
    }
 }

crossinline 需要配合 inline 的使用, 主要用于函数类型参数, 防止函数类型参数传递的是 lambda 函数体时, 间接调用使用, 如果该间接调用lambda内有return, 该修饰符能够防止非局部返回出现, 导致函数穿透一层作用域返回 running 函数

上面的 noinlinecrossinline都不用我们去学, 等到报错, ide会提醒, 并修复的

lambda中的局部返回

带标签的return是什么?

类似于 for 循环中的 break

 val list = arrayListOf(Person("a", 1), Person("b", 2), Person("c", 3))
 for (person in list) {
    if (person.name == "b") {
       break
    }
    println(person)
 }
 list.forEach {
    if (it.name == "b") {
       return@forEach
    }
    println(it)
 }
 list.forEach label@ {
    if (it.name == "b") {
       return@label
    }
    println(it)
 }

上面这三种方式功能都差不多

标签是 标签 + @ 标注在 lambda 表达式之前 标签@{ /* lambda */ }

使用的时候是 return + @ + 标签 中间没有空格, 这是返回

带标签的 this

和前面一样, 对 lambda 做标签 标签@ , 然后在使用的时候可以 this@标签.xxxx

 println(StringBuilder().apply sb@ {
    listOf(1, 2, 3).apply {
       this@sb.append(this.toString())
    }
 })

this@sb.appendappend IDEA无法智能提示, 需要我们主动手写

匿名函数使用的是 局部返回

 inline fun running(funcType01: (String) -> Int, noinline funcType02: (Int) -> Unit): (Int) -> Unit {
    println(funcType01("zhazha"))
    return funcType02
 }
 ​
 fun main() {
    println("调用之前: ")
    running(fun(str: String): Int {
       println(str)
       return str.length
    }) {
       println(it * 100)
    }
    println("调用之后")
 }

image.png

匿名函数在之前的章节中学习过, 这里就不做详解了