theme: Chinese-red
高阶函数
是什么?
一种以另一个函数为参数、返回值或两者兼顾的函数叫高阶函数
函数类型
整数类型, 可以存放整数, 字符串类型可以存放字符串, 而函数类型则可以存放函数引用
首先, 函数是虚拟的存在, 所以没有类型, 也没有对象 但是我们可以将函数的
参数列表和返回值类型归纳为一种实化类型 只要有了实化类型, 就会有对应的对象
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 就是函数类型
函数类型的参数名称
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 函数之后")
}
- 把函数体的lambda表达式内联掉
inline fun f(param: () -> Unit): Int {
println("inline function")
println("lambda") // 内联 lambda 表达式
return Random(100).nextInt()
}
- 把函数体拷贝到调用该内联函数的位置, 把 内联函数的
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 函数之后"), 但实际上, 它执行了.
我们把代码改成f{ println("lambda") }:
调用函数的返回值不会被内联函数直接内联到调用出, 而是去除 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
如果这段代码没有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, 但返回的是外部调用它的函数, 内联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, 其实是调用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函数
所以上面的提示就是让你修改 main 函数的返回值
我们可以给 return 表达式一个标签return @running it, 返回的是running函数
noinline用于未调用的lambda函数类型参数
防止照抄内联函数参数的参数名称到 main 函数中, 导致无法识别的问题
比如:
fun main() {
println("zhazha")
return funcType02; // 这能不报错?
}
它在下面是 running 函数的 参数名funcType02: (Int) -> Unit
此时我们可以添加noinline 关键字
inline fun running(funcType01: (String) -> Unit, noinline funcType02: (Int) -> Unit) {
funcType01("zhazha")
return funcType02
}
fun main() {
running({ println(it) }, { println(it * 10) })
}
注意, 如果内联函数的 lambda 参数没有被调用, 最好使用使用 noinline 的话, 否则将会变成这样:
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 函数
上面的
noinline和crossinline都不用我们去学, 等到报错,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.append的appendIDEA无法智能提示, 需要我们主动手写
匿名函数使用的是 局部返回
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("调用之后")
}
匿名函数在之前的章节中学习过, 这里就不做详解了