阅读 99

八、kotlin的高阶函数

高阶函数

是什么?

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

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 了么? 看上面我故意给的错误的字节码, 你会发现 println("函数执行之后") 这段代码将会不执行, 但实际情况这段代码可以执行, 所以证明 inline 函数的代码也不是直接拷贝过来的, 而是进行特殊处理

为什么要引入内联函数?

答: 让内联函数中的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, 但返回的是外部调用它的函数, 内联 lambda 的return会穿透一层作用域

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穿透一层外层作用域

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

解决方式也很简单, return@running it 完事, 这样他返回的是 running 函数, 而不是 main 函数, 这种方式被叫做 标签 和以前调用父类的方法一个很像

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

noinline 被用于lambda表达式(实参)传递给内联函数的函数类型参数(形参)时, 如果该 lambda 没有被调用, 而是用于 return 或者 存储于变量 就必须使用 noinline 标记, 否则会因为内联的存在导致 函数类型参数名被照抄到过去, 导致外部函数无法识别

image.png

inline fun running(funcType01: (String) -> Unit, noinline funcType02: (Int) -> Unit) {
    funcType01("zhazha")
    return funcType02
}

fun main() {
    running({ println(it) }, { println(it * 10) })
}
复制代码

image.png

如果不使用 noinline 的话, 将会变成这样:

image.png

crossinline 的使用场景

主要用于 函数参数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 函数

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

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

文章分类
后端
文章标签