Kotlin 高阶函数

定义高阶函数

高阶函数和 Lamdba 的关系是密不可分的。像接收 Lambda 参数的函数就可以称为具有函数式编程风格的 API,而如果想定义自己的函数式 API,那就得借助高阶函数来实现了。

高阶函数的定义:如果一个函数接收另一个函数作为参数,或者返回值的类型是另一个函数,那么该函数就称为高阶函数。(这里的另一个函数指的是函数类型,就像整型等。)

函数类型的基本语法规则:(String, Int) -> Unit

-> 左边部分用来声明该函数接收什么参数,多个参数之间用逗号隔开,如果不接收任何参数,用空括号表示。而 -> 右边部分用于声明该函数的返回值类型,如果没有返回值就使用 Unit,它大致相当于 Java 中的 void。

高阶函数的定义:将函数类型添加到某个函数的参数声明或者返回值声明上,这个函数就是一个高阶函数了。

1
2
3
fun example (func:  (String, Int) -> Unit){
func("hello", 123)
}

高阶函数的用途:高阶函数允许让函数类型的参数来决定函数的执行逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
fun main(){
val num1 = 100
val num2 = 80
// ::plus 这是一种函数引用方式的写法,表示将 plus() 函数作为参数传递给 num1AndNum2() 函数。
val result1 = num1AndNum2(num1,num2,::plus)
val result2 = num1AndNum2(num1,num2,::minus)
println("result1 is $result1")
println("result2 is $result2")
}

/**
* 定义高阶函数
* 第三个参数是一个接收两个整型参数并且返回值也是整型的函数类型参数。
*/
fun num1AndNum2(num1:Int, num2:Int, operation:(Int,Int) -> Int):Int{
val result = operation(num1,num2)
return result
}
fun plus(num1: Int,num2: Int):Int{
return num1 + num2
}
fun minus(num1: Int,num2: Int):Int{
return num1 - num2
}

Kotlin 中还支持其他多种方式来调用高阶函数,比如 Lambda 表达式、匿名函数、成员引用等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
fun main(){
val num1 = 100
val num2 = 80
// 使用 Lambda 表达式的写法来实现
// Lambda 表达式同样可以完整地表达一个函数的参数声明和返回值声明,而且写法更加精简。
// (Lambda 表达式中的最后一行代码会自动作为返回值)
val result1 = num1AndNum2(num1,num2){ n1, n2 ->
n1 + n2
}
val result2 = num1AndNum2(num1,num2){ n1, n2 ->
n1 - n2
}
println("result1 is $result1")
println("result2 is $result2")
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
fun main(){
val list = listOf("Apple","Banana","Pear")
val result = StringBuilder().build {
append("Start eating fruits.\n")
for (fruit in list){
append(fruit).append("\n")
}
append("Ate all fruits.")
}
println(result.toString())
}

/**
* 实现一个类似 apply() 函数的功能,只不过此函数只能作用在 StringBuilder 类上,需要借助泛型才能达到 apply() 作用在所有类的效果。
* 给 StringBuilder 类定义了一个 build 扩展函数,此函数接收一个函数类型参数,并且返回值类型也是 StringBuilder。
* StringBuilder. 的语法结构是定义高阶函数完整的语法规则,在函数类型的前面加上 ClassName,表示这个函数类型是定义在哪个类当中
* 将函数类型定义到 StringBuilder 类当中的好处就是,
* 当调用 build() 时传入的 Lambda 表达式将会自动拥有 StringBuilder 的上下文,同时这也是 apply 函数的实现方式。
*/
fun StringBuilder.build(block: StringBuilder.() -> Unit): StringBuilder{
block()
return this
}

内联函数的作用

高阶函数的实现原理

Kotlin 的代码最终还是要编译成 Java 字节码的,但 Java 中并没有高阶函数的概念。

Kotlin 的编译器会将这些高阶函数的语法转换成 Java 支持的语法结构,而 Lambda 表达式在底层被转换成了匿名类的实现方式。每调用一次 Lambda 表达式,都会创建一个新的匿名类实例,当然也会造成额外的内存和性能开销。

而内联函数的功能,可以将使用 Lambda 表达式带来的运行时开销完全消除。

1
2
3
4
5
6
7
8
/**
* 内联函数的用法:
* 只需要在定义高阶函数时加上 inline 关键字的声明
**/
inline fun num1AndNum2(num1:Int, num2:Int, operation:(Int,Int) -> Int):Int{
val result = operation(num1,num2)
return result
}

内联函数的工作原理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 原代码
* Kotlin 编译器会将内联函数中的代码在编译的时候自动替换到调用它的地方,这样也就不存在运行时的开销了。
*/
fun main(){
val num1 = 100
val num2 = 80
val result = num1AndNum2(num1,num2){n1,n2 ->
n1 + n2
}
println(result)
}

fun num1AndNum2(num1:Int, num2:Int, operation:(Int,Int) -> Int):Int{
val result = operation(num1,num2)
return result
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**
* 工作原理
* 首先,Kotlin 编译器会将 Lambda 表达式中的代码替换到函数类型参数调用的地方。
*/
fun main(){
val num1 = 100
val num2 = 80
val result = num1AndNum2(num1,num2)
println(result)
}

fun num1AndNum2(num1:Int, num2:Int):Int{
val result = num1 + num2
return result
}


/**
* 然后,再将内联函数中的全部代码替换到函数调用的地方。
* 正因如此,内联函数才能完全消除 Lambda 表达式所带来的的运行时开销。
*/
fun main(){
val num1 = 100
val num2 = 80
val result = num1 + num2
println(result)
}

noinline 与 crossinline

一个高阶函数中如果接收了两个或者更多函数类型的参数,这时给函数加上了 inline 关键字,那么 Kotlin 编译器会自动将所有引用的 Lambda 表达式全部进行内联。

但如果只想内联其中的一个 Lambda 表达式,可以使用 noinline 关键字。

1
2
3
4
/**
* 只会对 block1 参数所引用的 Lambda 表达式进行内联
*/
inline fun inlineTest(block1:() -> Unit, noinline block2:() -> Unit){}

noinline 关键字的意义在于:内联的函数类型参数在编译的时候会被进行代码替换,因此它没有真正的参数属性。非内联的函数类型参数可以自由地传递给其它任何函数,因为它就是一个真实的参数,而内联的函数类型参数只允许传递给另外一个内联函数,这也是它最大的局限性。

内联函数和非内联函数还有一个重要的区别:内联函数所引用的 Lambda 表达式中是可以使用 return 关键字来进行函数返回的,而非内联函数只能进行局部返回。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/**
* 非内联函数
*/
fun main(){
println("main start")
val str = ""
printString(str){ s ->
println("lambda start")
// 非内联函数只能使用 return@printString 的写法,表示进行局部返回,
// 并且不再执行 Lambda 表达式的剩余部分代码。
if (s.isEmpty()) return@printString
println(s)
println("lambda end")
}
println("main end")

// 打印结果:
// main start
// printString begin
// lambda start
// printString end
// main end
// 除了 Lambda 表达式中 return@printString 语句之后的代码没有打印,其它的日志是正常打印的,说明 return@printString 确实只能进行局部返回。
}

fun printString(str:String,block:(String) -> Unit){
println("printString begin")
block(str)
println("printString end")
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/**
* 内联函数
*/
fun main(){
println("main start")
val str = ""
printString(str){ s ->
println("lambda start")
// 除了 return@printString,内联函数是可以使用 return 关键字的,
// 此时的 return 代表的是返回外层的调用函数,也就是 main 函数
if (s.isEmpty()) return
println(s)
println("lambda end")
}
println("main end")

// 打印结果:
// main start
// printString begin
// lambda start
// 不管是 main()还是 printString(),在 return 关键字之后的都停止执行了。
}

inline fun printString(str:String,block:(String) -> Unit){
println("printString begin")
block(str)
println("printString end")
}

将高阶函数声明成内联函数是一种良好的编程习惯,事实上,绝大多数高阶函数是可以直接声明成内联函数的,但是也有少部分例外的情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/**
* 加上 inline 后,block() 会报错。
*
* 内联函数所引用的 Lambda 表达式允许使用 return 关键字进行函数返回,
* 但是由于是在匿名类中调用的函数类型参数,此时是不可能进行外层调用函数返回的,
* 最多只能对匿名类中的函数调用进行返回,因此这里就提示了错误。
*
* 也就是说,在高阶函数中创建了另外的 Lambda 或者匿名类的实现,并且在这些实现中调用函数类型参数,
* 此时再将高阶函数声明成内联函数,就一定会提示错误。借助 crossinline 关键字可解决这个问题。
*/
inline fun runRunnable(block:() -> Unit){
// 在函数中创建了一个 Runnable 对象,并在表达式中调用了传入的函数类型参数
// 而 Lambda 表达式在编译的时候会被转换成匿名类的实现方式,
// 也就是说,实际上是在匿名类中调用了传入的函数类型参数。
val runnable = Runnable{
block()
}
runnable.run()
}

/**
* 上述错误是因为内联函数的 Lambda 表达式中允许使用 return 关键字,
* 和高阶函数的匿名类实现中不允许使用 return 关键字之间造成了冲突。
*
* crossinline 关键字的意义在于:
* 它就像一个契约,用于保证在内联函数的 Lambda 表达式中一定不会使用 return 关键字,
* 但仍可以使用 return@printString 的写法进行局部返回。
*
* 总体来说,除了 return 关键字的使用上有所区别之外,crossinline 保留了内联函数的其他所有特性。
*/
inline fun runRunnable(crossinline block:() -> Unit){
val runnable = Runnable{
block()
}
runnable.run()
}

高阶函数的应用

高阶函数非常适用于简化各种 API 的调用,一些 API 的原有用法在使用高阶函数简化之后,不管是在易用性还是可读性方面,都可能会有很大的提升。

简化 SharedPreferences 的用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 通过扩展函数的方式向 SP 中添加了一个 open 函数,并且它还接收一个函数类型的参数,因此 open 函数自然就是一个高阶函数。
*
* 由于 open 函数内拥有 SP 的上下文,因此这里可直接调用 edit() 来获取 SharedPreferences.Edit() 对象。
*
* 另外 open 函数接收的是一个 SharedPreferences.Editor 的函数类型参数,因此这里需要调用 editor.block() 对函数类型参数进行调用,
* 就可以在函数类型参数的具体实现中添加数据了。
*
* 最后还要调用 editor.apply() 来提交数据,从而完成数据存储操作。
*/
fun SharedPreferences.open(block: SharedPreferences.Editor.() -> Unit){
val editor = edit()
editor.block()
editor.apply()
}

使用方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
        // 存储数据
btnSave.setOnClickListener{
// val editor = getSharedPreferences("data", Context.MODE_PRIVATE).edit()
// editor.putString("name","Tom")
// editor.putInt("age",28)
// editor.putBoolean("married",false)
// editor.apply()

// 使用高阶函数的方式来简化 SP 的用法。
// getSharedPreferences("data",Context.MODE_PRIVATE).open {
// // 拥有 SharedPreferences.Editor 的上下文环境,因此这里可直接调用相应的 put 方法来添加数据。
// putString("name","Tom")
// putInt("age",28)
// putBoolean("married",false)
// }

// 实际上,Google 提供的 KTX 扩展库中已经包含了上述的简化方法,这个扩展库为:
// implementation 'androidx.core:core-ktx:1.3.0'
// 但通过上述方法来了解原理,更有助于以后对更多的 API 进行扩展。
getSharedPreferences("data",Context.MODE_PRIVATE).edit {
putString("name","Tom")
putInt("age",28)
putBoolean("married",false)
}
}

简化 ContentValues 的用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
/**
* 虽然从功能性上面看好像用不到高阶函数的知识,但从代码实现上,却可以结合高阶函数来进行进一步的优化
* apply 函数的返回值就是它的调用对象本身,因此这里可以使用单行代码函数的语法糖,用等号替代返回值的声明。
* 另外 ,apply 函数的 Lambda 表达式中会自动拥有 ContentValues 的上下文,所以可直接调用 put 方法。
*/
fun cvOf(vararg pairs: Pair<String, Any?>) = ContentValues().apply{
for (pair in pairs){
val key = pair.first
val value = pair.second
when(value){
// 这里还使用了 Kotlin 中的 Smart Cast 功能。
// 比如 when 语句进入 Int 条件分支后,这个条件下面的 value 会被自动转换成 Int 类型,而不再是 Any? 类型,
// 这样就不需要像 Java 中那样再额外进行一次向下转型了,这个功能在 if 语句中也同样适用。
is Int -> put(key, value)
is Long -> put(key, value)
is Short -> put(key, value)
is Float -> put(key, value)
is Double -> put(key, value)
is Boolean -> put(key, value)
is String -> put(key, value)
is Byte -> put(key, value)
is ByteArray -> put(key, value)
null -> putNull(key)
}
}
}

/**
* 这个方法的作用是构建一个 ContentValues 对象。
*
* mapOf() 函数允许使用 "Apple" to 1 这样的语法结构快速创建一个键值对。
* 在 Kotlin 中使用 A to B 这样的语法结构会创建一个 Pair 对象。
*
* 方法接收一个 Pair 参数,vararg 关键字对应的是 Java 中的可变参数列表,
* 允许向这个方法传入 0 个、1 个甚至任意多个 Pair 类型的参数,
* 这些参数都会被赋值到使用 vararg 声明的这一个变量上面,然后使用 for-in 循环可以将传入的所有参数遍历出来。
*
* Pair 是一种键值对的数据结构,因此需要通过泛型来指定它的键和值分别对应什么类型的数据。
* ContentValues 的键都是字符串类型,所以可直接将 Pair 键的泛型指定成 String,
* 但 ContentValues 的值可以有多种类型(字符串型、整型、浮点型、甚至是 null),所以要指定成 Any,
* Any 是 Kotlin 中所有类的共同基类,相当于 Java 的 Object,而 Any?表示允许传入空值。
*/
//fun cvOf(vararg pairs: Pair<String, Any?>): ContentValues{
// // 创建 ContentValues 对象
// val cv = ContentValues()
// // 遍历 pairs 参数列表,取出其中的数据并填入 ContentValues 中,最终将 ContentValues 对象返回.
// for (pair in pairs){
// val key = pair.first
// val value = pair.second
// // 使用 when 语句一一进行条件判断,并覆盖 ContentValues 所支持的所有数据类型。
// // (因为 Pair 参数的值是 Any?类型)
// when(value){
// // 这里还使用了 Kotlin 中的 Smart Cast 功能。
// // 比如 when 语句进入 Int 条件分支后,这个条件下面的 value 会被自动转换成 Int 类型,而不再是 Any? 类型,
// // 这样就不需要像 Java 中那样再额外进行一次向下转型了,这个功能在 if 语句中也同样适用。
// is Int -> cv.put(key, value)
// is Long -> cv.put(key, value)
// is Short -> cv.put(key, value)
// is Float -> cv.put(key, value)
// is Double -> cv.put(key, value)
// is Boolean -> cv.put(key, value)
// is String -> cv.put(key, value)
// is Byte -> cv.put(key, value)
// is ByteArray -> cv.put(key, value)
// null -> cv.putNull(key)
// }
// }
// return cv
//}

使用方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//                val values = ContentValues()
// values.put("name","Game of Thrones")
// values.put("author","George Martin")
// values.put("pages",720)
// values.put("price",20.85)

// 使用 apply 函数简化写法
// val values = ContentValues().apply {
// put("name","Game of Thrones")
// put("author","George Martin")
// put("pages",720)
// put("price",20.85)
// }

// 使用高阶函数来简化用法
// val values = cvOf("name" to "Game of Thrones","author" to "George Martin",
// "pages" to 720,"price" to 20.85)

// 实际上,KTX 库中也提供了一个同样功能的方法
val values = contentValuesOf("name" to "Game of Thrones","author" to "George Martin",
"pages" to 720,"price" to 20.85)

db.insert("Book",null,values)

备注

参考资料

第一行代码(第3版)
官方文档
官方中文翻译站

传送门GitHub