高阶函数的定义。如果一个函数接收另一个函数作为参数,或者返回值的类型是另一个函数,那么该函数就称为高阶函数。
接下来我们就学习一下如何定义一个函数类型。不同于定义一个普通的字段类型,函数类型的语法规则是有点特殊的,基本规则如下:
(String, Int) -> Unit
既然是定义一个函数类型,那么最关键的就是要声明该函数接收什么参数,以及它的返回值是什么。因此,->左边的部分就是用来声明该函数接收什么参数的,多个参数之间使用逗号隔开,如果不接收任何参数,写一对空括号就可以了。而->右边的部分用于声明该函数的返回值是什么类型,如果没有返回值就使用Unit,它大致相当于Java中的void。
fun example(num1: Int, num2: Int, func: (Int, Int) -> Int) {
val result = func(num1, num2)
}
fun plus(num1: Int, num2: Int) = num1 + num2
fun main() {
// 成员引用
print(example(11, 22, ::plus))
// Lambda表达式
print(example(11, 33) { a, b -> a + b })
}
参数使用了::plus,这是一种函数引用方式的写法,表示将plus()函数作为参数传递给example()函数。
此Kotlin还支持其他多种方式来调用高阶函数,比如Lambda表达式、匿名函数、成员引用等。
apply函数,它可以用于给Lambda表达式提供一个指定的上下文,当需要连续调用同一个对象的多个方法时,apply函数可以让代码变得更加精简,比如StringBuilder就是一个典型的例子。接下来我们就使用高阶函数模仿实现一个类似的功能。
fun StringBuilder.build(block: StringBuilder.() -> Unit): StringBuilder {
block()
return this
}
fun main() {
val list = listOf("Orange", "Apple")
val sb = StringBuilder().build {
append("start")
for (fruit in list) append(fruit)
append("end")
}
}
这个函数类型参数的声明方式和我们前面学习的语法有所不同:它在函数类型的前面加上了一个StringBuilder. 的语法结构。这是什么意思呢?其实这才是定义高阶函数完整的语法规则,在函数类型的前面加上ClassName.就表示这个函数类型是定义在哪个类当中的。
那么这里将函数类型定义到StringBuilder类当中有什么好处呢?好处就是当我们调用build函数时传入的Lambda表达式将会自动拥有StringBuilder的上下文,同时这也是apply函数的实现方式。
高阶函数的实现原理
fun num1AndNum2(num1: Int, num2: Int, operation: (Int, Int) -> Int): Int {
val result = operation(num1, num2)
return result
}
fun main() {
val num1 = 100
val num2 = 80
val result = num1AndNum2(num1, num2) { n1, n2 ->
n1 + n2
}
}
fun num1AndNum2(num1: Int, num2: Int, operation: Function): Int {
return operation.invoke(num1, num2)
}
fun main() {
val num1 = 100
val num2 = 80
val result = num1AndNum2(num1, num2, object : Function() {
operator fun invoke(n1: Int, n2: Int): Int? {
return n1 + n2
}
})
}
这就是Kotlin高阶函数背后的实现原理。你会发现,原来我们一直使用的Lambda表达式在底层被转换成了匿名类的实现方式。这就表明,我们每调用一次Lambda表达式,都会创建一个新的匿名类实例,当然也会造成额外的内存和性能开销。为了解决这个问题,Kotlin提供了内联函数的功能,它可以将使用Lambda表达式带来的运行时开销完全消除。
内联函数的用法非常简单,只需要在定义高阶函数时加上inline关键字的声明即可
inline fun num1AndNum2(num1: Int, num2: Int, operation: (Int, Int) -> Int): Int {
val result = operation(num1, num2)
return result
}
那么内联函数的工作原理又是什么呢?其实并不复杂,就是Kotlin编译器会将内联函数中的代码在编译的时候自动替换到调用它的地方,这样也就不存在运行时的开销了。
noinline
一个高阶函数中如果接收了两个或者更多函数类型的参数,这时我们给函数加上了inline关键字,那么Kotlin编译器会自动将所有引用的Lambda表达式全部进行内联。
但是,如果我们只想内联其中的一个Lambda表达式该怎么办呢?这时就可以使用noinline关键字了,如下所示:
inline fun inlineTest(block1: () -> Unit, noinline block2: () -> Unit) {
}
前面我们已经解释了内联函数的好处,那么为什么Kotlin还要提供一个noinline关键字来排除内联功能呢?这是因为内联的函数类型参数在编译的时候会被进行代码替换,因此它没有真正的参数属性。非内联的函数类型参数可以自由地传递给其他任何函数,因为它就是一个真实的参数,而内联的函数类型参数只允许传递给另外一个内联函数,这也是它最大的局限性。
另外,内联函数和非内联函数还有一个重要的区别,那就是内联函数所引用的Lambda表达式中是可以使用return关键字来进行函数返回的,而非内联函数只能进行局部返回。为了说明这个问题,我们来看下面的例子。
将printString()函数声明成一个非内联函数
fun printString(str: String, block: (String) -> Unit) {
println("start")
block(str)
println("end")
}
fun main() {
println("main start")
printString("") { s ->
println("lambda start")
if (s.isEmpty()) return@printString
println("lambda end")
}
println("main end")
}
// 执行结果
main start
start
lambda start
end
main end
这里定义了一个叫作printString()的高阶函数,用于在Lambda表达式中打印传入的字符串参数。但是如果字符串参数为空,那么就不进行打印。注意,Lambda表达式中是不允许直接使用return关键字的,这里使用了return@printString的写法,表示进行局部返回,并且不再执行Lambda表达式的剩余部分代码。
将printString()函数声明成一个内联函数
inline fun printString(str: String, block: (String) -> Unit) {
println("start")
block(str)
println("end")
}
fun main() {
println("main start")
printString("") { s ->
println("lambda start")
if (s.isEmpty()) return
println("lambda end")
}
println("main end")
}
// 执行结果
main start
start
lambda start
现在printString()函数变成了内联函数,我们就可以在Lambda表达式中使用return关键字了。此时的return代表的是返回外层的调用函数,也就是main()函数
crossinline关键字
// 会报错
inline fun runRunnable(block: () -> Unit) {
val runnable = Runnable {
block()
}
runnable.run()
}
// 声明crossinline就不会报错了
inline fun runRunnable(crossinline block: () -> Unit) {
val runnable = Runnable {
block()
}
runnable.run()
}
因为内联函数的Lambda表达式中允许使用return关键字,和高阶函数的匿名类实现中不允许使用return关键字之间造成了冲突。而crossinline关键字就像一个契约,它用
于保证在内联函数的Lambda表达式中一定不会使用return关键字,这样冲突就不存在了,问题也就巧妙地解决了。
声明了crossinline之后,我们就无法在调用runRunnable函数时的Lambda表达式中使用return关键字进行函数返回了,但是仍然可以使用return@runRunnable的写法进行局部返回。总体来说,除了在return关键字的使用上有所区别之外,crossinline保留了内联函数的其他所有特性。
普通用法
fun add(context: Context) {
val edit = context.getSharedPreferences("data", Context.MODE_PRIVATE).edit()
edit.putString("1", "1")
edit.putString("1", "1")
edit.putString("1", "1")
edit.apply()
}
使用高阶函数优化
fun SharedPreferences.open(block: SharedPreferences.Editor.() -> Unit) {
val editor = edit()
editor.block()
editor.apply()
}
通过扩展函数的方式向SharedPreferences类中添加了一个open函数,并且它还接收一个函数类型的参数,因此open函数自然就是一个高阶函数了。
由于open函数内拥有SharedPreferences的上下文,因此这里可以直接调用edit()方法来获取SharedPreferences.Editor对象。另外open函数接收的是一个SharedPreferences.Editor的函数类型参数,因此这里需要调用editor.block()对函数类型参数进行调用,我们就可以在函数类型参数的具体实现中添加数据了。最后还要调用editor.apply()方法来提交数据,从而完成数据存储操作。
fun main(context: Context) {
context.getSharedPreferences("data", Context.MODE_PRIVATE).open {
putString("1", "1")
putString("1", "1")
putString("1", "1")
putString("1", "1")
}
}
Google的KTX库中已经自带了一个edit函数
getSharedPreferences("data", Context.MODE_PRIVATE).edit {
putString("name", "Tom")
putInt("age", 28)
putBoolean("married", false)
}
普通用法
val values = ContentValues()
values.put("name", "Game of Thrones")
values.put("author", "George Martin")
values.put("pages", 720)
values.put("price", 20.85)
db.insert("Book", null, values)
使用高阶函数优化
fun cvOf(vararg pairs: Pair<String, Any?>) = ContentValues().apply {
for (pair in pairs) {
val key = pair.first
val value = pair.second
when (value) {
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)
}
}
}
fun main() {
val values = cvOf(
"name" to "Game of Thrones",
"author" to "George Martin",
"pages" to 720,
"price" to 20.85
)
}
cvOf()方法接收了一个Pair参数,也就是使用A to B语法结构创建出来的参数类型,但是我们在参数前面加上了一个vararg关键字,这是什么意思呢?其实vararg对应的就是Java中的可变参数列表,我们允许向这个方法传入0个、1个、2个甚至任意多个Pair类型的参数,这些参数都会被赋值到使用vararg声明的这一个变量上面,然后使用for-in循环可以将传入的所有参数遍历出来。
再来看声明的Pair类型。由于Pair是一种键值对的数据结构,因此需要通过泛型来指定它的键和值分别对应什么类型的数据。值得庆幸的是,ContentValues的所有键都是字符串类型的,这里可以直接将Pair键的泛型指定成String。但ContentValues的值却可以有多种类型(字符串型、整型、浮点型,甚至是null),所以我们需要将Pair值的泛型指定成Any?。这是因为Any是Kotlin中所有类的共同基类,相当于Java中的Object,而Any?则表示允许传入空值。
接下来我们开始为cvOf()方法实现功能逻辑,核心思路就是先创建一个ContentValues对象,然后遍历pairs参数列表,取出其中的数据并填入ContentValues中,最终将
ContentValues对象返回即可。