【kotlin】高阶函数详解

文章目录

  • 定义高阶函数
  • 内联函数的作用
  • 高级函数的应用
    • 简化SharedPreferences 的用法
    • 简化 ContentValues 的用法

定义高阶函数

如果一个函数接收另一个函数作为参数,或者返回值的类型是另一个函数,那么该函数就称为高阶函数

这就涉及到另一个概念了:函数类型。下面学习一下如何定义一个函数类型,基本规则如下:

(String,Int) -> Unit

很晕吧,听我细细讲来。既然是定义一个函数类型,那么最关键的是声明该函数接收什么参数,以及它的返回值是什么。因此,左边部分就是用来声明该函数接收什么参数的,多个参数用逗号隔开,如果不接收任何参数,写一对空括号就可以了。而右边部分用于声明该函数的返回值类型,如果没有返回值就用 Unit,大致相当于 Java 中的 void

现在将上述函数类型添加到某个函数的参数声明或者返回值声明上,那么这个函数就是一个高阶函数了,例如

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

可以看到这里的 example() 函数接收到了一个函数类型参数,因此 example() 函数就是一个高阶函数

下面举例来说明下。这里我准备定义一个叫作 num1AndNum2() 的高阶函数,让它接收两个整形和一个函数类型的参数。我们会在 num1AndNum2() 函数中对传入的两个整型参数进行某种运算,并返回运行结果。但具体进行什么运算是由传入的函数类型参数决定的

新建一个名为 HigherOrderFunction.kt 文件

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
}

接着在这个文件写一个 main() 函数

fun main(){
    val num1 = 100
    val num2 = 80
    val result1 = num1AndNum2(num1,num2, ::plus)
    val result2 = num1AndNum2(num1,num2,::minus)
    println("result1:"+result1)
    println("result2:"+result2)
}

运行结果
在这里插入图片描述
第三个参数使用了::plus::minus 的写法,这是一种函数引用方式的写法,表示将 plus() 和 minus() 函数作为参数传递给 num1AndNum2() 函数

如果每次调用任何高阶函数时都还得先定义一个与其函数类型参数相匹配的函数,是不是太复杂了?没错,因此 Kotlin 还支持其他多种方式来调用高阶函数,比如 Lambda 表达式、匿名函数、成员引用等。其中 Lambda 表达式是最常见也是最普遍的高阶函数调用方式,刚才的代码使用 Lambda 表达式来实现(Lambda 表达式最后一行自动作为返回值),plus() 和 minus() 函数可以删掉了

fun main() {
    val num1 = 100
    val num2 = 80
    val result1 = num1AndNum2(num1, num2) { n1, n2 ->
        n1 + n2
    }
    val result2 = num1AndNum2(num1, num2) { n1, n2 ->
        n1 - n2
    }
    println("result1:" + result1)
    println("result2:" + result2)
}

fun num1AndNum2(num1: Int, num2: Int, operation: (Int, Int) -> Int): Int {
    val result = operation(num1, num2)
    return result
}

下面,我们回想 apply 函数,它可以用于给 Lambda 表达式提供一个指定的上下文,当需要连续调用同一个对象的多个方法时,apply 函数可以让代码变得更加精简。我们用高阶函数模仿 StringBuilder 的功能

现在创建一个 HigherOrderFunction.kt 的文件

fun StringBuilder.build(block:StringBuilder.()->Unit):StringBuilder{
    block()
    return this
}

这里我们给 StringBuilder 类定义了一个 build 扩展函数,这个扩展函数接收一个函数类型参数,并且返回值类型也是 StringBuilder。其中 StringBuilder. 的语法结构是什么意思呢?其实这才是定义高阶函数完整的语法规则,在函数类型前面加上 ClassName. 就表示这个函数类型是定义在哪个类当中的。这里我们把函数类型定义到 StringBuilder 类当中了,我们调用 build 函数时传入 Lambda 表达式将自动拥有 StringBuilder 的上下文

fun main(){
    val list = listOf("Apple","Banana","Orange","Pear","Grape")
    val result = StringBuilder().build{
        append("Start eating fruit.\n")
        for(fruit in list){
            append(fruit).append("\n")
        }
        append("Ate all fruits")
    }
    println(result.toString())
}

【kotlin】高阶函数详解_第1张图片

内联函数的作用

我们一直使用的 Lambda 表达式在底层被转换成了匿名类的实现方式。这就表明,我们每调用一次 Lambda 表达式,都会创建一个新的匿名类实例,当然也会造成额外的内存和性能开销。为了解决这个问题,Kotlin 提供了内联函数的功能,它可以将使用 Lambda 表达式带来的运行时开销完全消除,只需要在定义高阶函数时加上 inline 关键字的声明即可:

inline fun num1AndNum2(num1: Int, num2: Int, operation: (Int, Int) -> Int): Int {
    val result = operation(num1, num2)
    return result
}

下面讨论一些更特殊的情况,比如,一个高阶函数中如果接收了两个或者更多函数类型的参数,这时我们给函数加上 inline关键字,那么 Kotlin 编译器会自动将所有引用的 Lambda 表达式全部内联。但如果只想内联其中一个 Lambda 表达式,就可以使用 noinline 关键字了

inline fun inlineTest(block1:()->Unit,noinline block2:()->Unit){
}

刚才讲过了 inline 函数的好处了,为什么还要提供 noinline 关键字来排除内联功能呢?这是因为内联的函数类型参数在编译的时候会被进行代码替换,因此它没有真正的参数属性。非内联的函数类型参数可以自由地传递给其他任何函数,因为它就是一个真实的参数,而内联的函数类型参数只允许传递给另外一个内联函数,这也是它最大的局限性

另外,内联函数和非内联函数还有一个重要的区别,那就是内联函数所引用的Lambda表达式中是可以使用return关键字来进行函数返回的,而非内联函数只能进行局部返回。为了说明这个问题,我们来看下面的例子

fun main() {
  println("main start")
    val str = ""
    printString(str){s->
        println("lambda start")
        if(s.isEmpty()) return@printString
        println(s)
        println("lambda end")
    }
    println("main end")
}

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

这里定义了一个 printString() 的高阶函数,用于在 Lambda 表达式中打印传入的字符串参数,但是如果字符串参数为空,那么就不进行打印了。Lambda 表达式中是不允许直接使用 return 关键字的,这里使用了 return@printString 的写法,表示进行局部返回,并且不再执行 Lambda 表达式的剩余部分代码

当我们传入空字符串时:
【kotlin】高阶函数详解_第2张图片
但如果我们将 printString() 函数声明成一个内联函数,情况就不一样了:

fun main() {
    println("main start")
    val str = ""
    printString(str){s->
        println("lambda start")
        if(s.isEmpty()) return
        println(s)
        println("lambda end")
    }
    println("main end")
}

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

现在 printString() 函数变成了内联函数,我们可以在 Lambda表达式中使用 return 关键字了。此时的 return 代表的是返回外层的调用函数,也就是 main() 函数,重新运行程序:

【kotlin】高阶函数详解_第3张图片
将高阶函数声明成内联函数是一种良好的编程习惯,事实上,绝大多数高阶函数是可以直接声明成内联函数的,但是也有少数部分例外的情况,观察下面代码:

inline fun runRunnable(block:() -> Unit){
    val runnable = Runnable{
        block()
    }
    runnable.run()
}

【kotlin】高阶函数详解_第4张图片

首先在 runRunnable() 函数中,我们创建了一个 Runnable 对象,并在 Runnable 的 Lambda 表达式中调用了传入的函数类型参数。而 Lambda 表达式在编译的时候会被转换成匿名类的实现方式,也就是说,上述代码实际上是在匿名类中调用了传入的函数类型参数
而内联函数所引用的 Lambda 表达式允许使用 return 关键字进行函数返回,但是由于我们实在匿名类中调用的函数类型参数,此时是不可能进行外层调用函数返回的,最多只能对匿名类中的函数进行返回,因此提示了上述错误
如果我们在高阶函数中创建了另外的 Lambda 或者 匿名类的实现,并且在这些实现中调用函数类型参数,此时再将高阶函数声明成内联函数,就一定会提示错误

我们可以借助 crossinline关键字可以解决这个问题

【kotlin】高阶函数详解_第5张图片
之所以会提示那个错误,就是因为内联函数的 Lambda 表达式中允许使用 return 关键字,和高阶函数的匿名类中不允许使用 return 关键字之间造成了冲突,而 crossinline关键字像一个契约,它用于保证在内联函数的 Lambda表达式中一定不会使用 return 关键字,这样冲突就不存在了

声明了crossinline之后,我们就在无法调用 runRunnable 函数时的 Lambda表达式中使用 return 进行函数返回了,但仍可以使用 return@runRunnable 的写法进行局部返回。总体来说,除了在 return 关键字的使用上有所区别之外, crossinline保留了内联函数的其他所有特性

高级函数的应用

简化SharedPreferences 的用法

首先回顾下 SharedPreferences 的用法

val editor = getSharedPreferences("data", Context.MODE_PRIVATE).edit()
editor.putString("name","Tom")
editor.putInt("age",28)
editor.putBoolean("married",false)
editor.apply()

这段代码本身已足够简单,但这种写法更多还是在用 Java 的编程思维来编写代码,而在 Kotlin 中我们明显可以做的更好,我们尝试使用高阶函数简 SharedPreferences 的用法,新建 SharedPreferences.kt 文件,然后在里面加入如下代码:

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() 方法来提交数据,从而完成数据存储操作

定义好 open 函数之后,使用 SharedPreferences 存储数据写法如下:

getSharedPreferences("data",Context.MODE_PRIVATE).open {
	putString("name", "Tom")
	putInt("age", 28)
	putBoolean("married", false)
}

其实 Google 提供的 KTX 扩展库中已经包含了上述 SharedPreferences 的简化用法,这个扩展库会在 AS 创建项目的时候自动引入 build.gradle 的 dependencies 中
【kotlin】高阶函数详解_第6张图片
因此我们可以直接在项目中这样写

getSharedPreferences("data",Context.MODE_PRIVATE).edit {
	putString("name", "Tom")
	putInt("age", 28)
	putBoolean("married", false)
}

简化 ContentValues 的用法

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)

我们可以用 apply 函数进行简化,但是可以更好。首先你应该知道 Kotlin 中使用 A to B 这样的语法结构会创建一个 Pair 对象

新建一个 ContentValues.kt 文件,然后定义一个 cvof() 方法,如下:

fun cvOf(vararg pairs:Pair<String,Any?>):ContentValues{
}

这个方法的作用是构建一个 ContentValues 对象。cvOf()方法接收了一个 Pair 参数,也就是使用 A to B 语法结构创建出来的参数类型,但是我们在参数前加上了一个 vararg 关键字,其实 vararg 对应的就是 Java 中的可变参数列表,我们允许向这个方法传入0个,1个,2个甚至任意多个 Pair 类型的参数,这些参数都会被赋值到使用 vararg 声明的这一个变量上面,然后使用 for-in 循环可以将传入的所有参数遍历出来

再看Pair 类型,由于 Pair 是一种键值对的数据结构,因此需要通过泛型来指定它的键和值分别对应什么类型的数据。值得庆幸的是,ContentValues 的所有键都是字符串类型的,这里可以直接将 Pair 键的泛型指定成 String。但 ContentValues 的值却可以有多种类型,所以我们需要将 Pair 值的泛型指定成 Any?。因为 Any 是 Kotlin 中所有类的共同基类,相当于 Java 中的 Object,而 Any? 则表示允许传入空值

接下来我们开始为 cvOf() 方法实现功能逻辑,核心思路就是先创建一个 ContentValues 对象,然后便利 pairs 参数列表,取出其中的数据并填入 ContentValues 中,最终将 ContentValues 对象返回即可。存在的一个问题是:Pair 参数类型是 Any? 类型的,我们怎样让它和 ContentValues 所支持的数据类型对应起来呢?没有什么好办法,只能使用 when 语句一一判断:

fun cvOf(vararg pairs:Pair<String,Any?>):ContentValues{
    val cv = ContentValues()
    for(pair in pairs){
        val key = pair.first
        val value = pair.second
        when(value){
            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
}

这里使用了 Kotlin 的 Smart Cast 功能,比如 when 语句进入 Int 条件分支后,这个条件下面的 value 会被自动转换成 Int,而不再是 Any? 类型,这样我们就不需要像 Java 中那样在额外进行一次向下转型了,这个功能在 if 语句中也同样适用

我们使用上面的方法:

val values = cvOf("name" to "Game of Thrones","author" to "George Martin","pages" to 720,"price" to 20.85)
db.insert("Book",null,values)

功能性方面,cvOf() 方法好像确实用不到高阶函数的只是,但从代码实现方面,却可以结合高阶函数来进行进一步优化。比如借助 apply 函数:

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)
        }
    }
}

由于 apply 函数的返回值就是它的调用对象本身,因此这里我们可以使用单行代码函数的语法糖,用等号代替返回值的声明。另外,apply 函数的 Lambda 表达式中会自动拥有 ContentValues 的上下文,所以这里可以直接调用 ContentValue 的各种 put 方法

同前,KTX 库中也提供了一个具有相同功能的 contentValuesOf() 方法:

val values = contentValuesOf("name" to "Game of Thrones","author" to "George Martin","pages" to 720,"price" to 20.85)
db.insert("Book",null,values)

你可能感兴趣的:(Kotlin从零到一无所有,kotlin,高阶函数)