前言
上一篇文章我们详细的介绍了Kotlin中函数的声明和使用,本篇文章给大家介绍Kotlin中的高阶函数和Lambda表达式。熟练的掌握高阶函数和Lambda表达式,在Kotlin开发中是很有必要的。下面开始我们本篇文章的学习。
1.函数类型
在Kotlin中允许我们定义一个函数类型。具体语法如下:
(参数类型, 参数类型...) -> 返回值类型
->左边是这个函数类型的参数列表,用()括起来,如果没有参数,直接使用()即可。多个参数使用逗号隔开,->右边是这个函数类型的返回值类型。
在上一篇函数的文章中介绍到函数的默认返回值类型Unit
,而我们在声明一个函数类型时,如果函数类型没有确切的返回值类型需要显示的指明为Unit
。
// 无参数,没有确切的返回值类型,返回一个Unit类型
() -> Unit
// 有两个参数,一个是Int类型,一个是String类型,返回一个String类型
(Int,String) -> String
在属性和控制流的文章中我们介绍到:当我们需要声明一个属性为可空时,我们只需要在属性类型后添加?
。而想要声明一个函数类型为可空时,亦是如此。我们只需要将函数类型用()括起来,然后在()后面添加?
即可。
// 函数类型可为空
(() -> Unit)?
// 函数类型可为空
((Int,String) -> String)?
选中当前项目,右击鼠标或者触摸板。New -> Kotlin File/Class,在弹出的选择框中,选中File创建一个HigherFunction.kt的文件。我们定义如下函数类型的属性:
// 延迟初始化,无需初始值
lateinit var block:() -> Unit
// 可空的函数类型
var block1:(() -> Unit)? = null
// 普通的函数类型初始化
var block2:(() -> Unit) = { }
从上面的代码中我们可以看到其实函数类型属性的声明和我们普通属性的声明在语法上都是一样的。支持延迟初始化,支持可空类型,默认情况下都必须设置初始值。
只是在声明的时候,我们将普通的属性类型,替换成了函数类型。
2.Lambda表达式
Lambda表达式的具体语法如下:
{ 参数名:参数类型, 参数名:参数类型 -> 函数体 }
Lambda表达式是用花括号 { } 括起来的一小段代码。->左边是Lambda表达式的参数列表,而-> 右边则是函数体。 Lambda表达式的最后一行代码是这个Lambda的返回值。这和我们在属性和控制流的文章中介绍的if
、when
表达式很类似,只是if
、when
表达式是执行分支块的最后一行代码作为这个表达式的返回值。
在Kotlin中我们使用Lambda表达式来完成对函数类型的初始化,如下示例代码:
val block: (Int, String) -> String = { a:Int, b: String -> "$a $b" }
我们使用Lambda表达式对函数类型block进行了初始化。和普通的属性初始化一样,我们用等号(=) 来连接函数类型和Lambda表达式。
当Lambda表达式参数列表有声明确切的类型,通常Kotlin编译器可以推断出该函数类型,我们可以在声明时将其省略。
val block = { a:Int, b: String -> "$a $b" }
当函数类型有确切声明时,Kotlin编译器也可以推断出Lambda中参数类型,我们也可以省略Lambda表达式中的参数类型。
val block:(Int,String) -> String = { a, b -> "$a $b" }
通常Lambda表达式在编写代码的时候,都必须按照语法定义的格式来:
{ 参数名:参数类型, 参数名:参数类型 -> 函数体 }
参数列表和 -> 都是必须要编写的,否者Kotlin编译器会报语法错误。
有一种常用且特殊的情况是,当函数类型有且只有一个参数类型时,我们在初始化这个函数类型的时候,可以在Lambda表达式的花括号中省略单个参数的声明和 ->。该参数将被隐式的声明为it。如下代码我们声明一个函数类型的参数block:
val block:(String) -> Unit = { println("it = $it") }
该函数拥有一个String类型的参数,我们可以在Lambda表达式的花括号中使用隐式名称it来访问该参数。
当然在实际开发的过程中,我们可能更想让我们的函数参数拥有实际的意义。我们也可以不使用隐式名称it,而是按照语法的定义来编写一个Lambda表达式。
val block:(String) -> Unit = { name: String -> println("name = $name") }
3.函数类型实例的调用:
介绍完函数类型和函数类型的初始化。我们再来看一下函数类型实例的调用。沿用上述示例中的函数类型block,我们在HigherFunction.kt的main函数中调用函数类型的实例block:
val block:(String) -> Unit = { println("it = $it") }
fun main() {
block("kotlin") // 或者block.invoke("kotlin")
}
可以看到对函数类型实例的调用和普通函数调用没有什么区别,都是在函数名后跟上小括号()。然后在()中传入在声明时的参数即可。
通常我们还可以使用funName.invoke()的方式来调用一个函数类型的实例。而invoke方法是调用操作符()的重载。具体的实现对应了kotlin.jvm.functions包下的Functions.kt文件中的Function2接口。
public interface Function2<in P1, in P2, out R> : Function<R> {
public operator fun invoke(p1: P1, p2: P2): R
}
关于操作符的重载后面的文章中会详细的介绍。这里我们只要了解调用一个函数类型的实例有两种方式就好。
block("kotlin") 和 block.invoke("kotlin") 都可以完成对函数类型实例block的调用
4.高阶函数
有了函数类型和Lambda表达式的基础,我们再来学习高阶函数就容易多了。首先我们看下官方文档中给出的高阶函数定义:高阶函数是将函数用作参数或返回值的函数。
更直白一点的翻译是,当一个函数它拥有函数类型的参数,或者它的返回值类型是函数类型,我们就称这个函数为高阶函数。如下我们定义一个高阶函数sum,并在main函数中去访问sum函数。
fun main() {
val left = 101
val right = 102
val block = { left:Int, right:Int -> left + right }
val result = sum(left, right, block)
println("result = $result")
}
fun sum(left:Int, right: Int,block: (Int, Int) -> Int): Int {
return block.invoke(left, right)
}
// 输出
result = 203
在sum函数中,我们通过对函数类型的实例block的调用来完成两个Int类型的数值求和。通常在Kotlin中我们对一个高阶函数的调用更习惯于用“匿名Lambda表达式”的方式,虽然官方没有明确的给出这种定义,事实上“匿名Lambda表达式”和Java中的匿名类如出一辙。只是在Java中我们需要通过关键字new来创建一个匿名类,而Kotlin则省略了创建对象时的关键字。
fun main() {
val left = 101
val right = 102
val result = sum(left, right, { left:Int, right:Int -> left + right })
println("result = $result")
}
fun sum(left:Int, right: Int,block: (Int, Int) -> Int): Int {
return block.invoke(left, right)
}
5.拖尾Lambda表达式
在 Kotlin 中:如果函数的最后一个参数是函数,那么作为相应参数传入的 lambda 表达式可以放在圆括号之外:
fun main() {
val left = 101
val right = 102
val result = sum(left, right) { left: Int, right :Int ->
left + right
}
println("result = $result")
}
fun sum(left:Int, right: Int,block: (Int, Int) -> Int): Int {
return block.invoke(left, right)
}
对于这种方式的语法调用我们称之为:拖尾Lambda表达式 。
如果高阶函数仅有一个函数类型的参数,我们在调用时还可以省略 (),直接在函数名后添加花括号 { }。如下我们定义的normal函数:
fun main() {
normal { println("the normal called") }
}
fun normal(block: () -> Unit) {
block.invoke()
}
// 输出
the normal called
6.带有上下文作用域的Lambda表达式
在上一篇函数的文章中我们介绍了扩展函数和扩展属性。而函数类型也是一样支持“可扩展”的,虽然官方文档中没有明确的说扩展函数类型,但是我觉得这也是对带有上下文作用域的Lambda表达式最好称呼了。可能这个称呼听起来就比较别扭吧,官方也就没有这么明确的定义。
而是给出了带有接收者的函数字面值这样的描述: A.(B) -> C。其实这和扩展属性和扩展函数不是一样的语法逻辑吗?我们把该函数类型定义在了某个具体的类中,叫“扩展函数类型”也不为过吧。A.(B)-> C,就表示我在类A中定义了一个 (B)-> C的函数类型。下面我们来看一个具体的应用:
val info: String.() -> Unit = {
val length = this.length
val isEmpty = this.isEmpty()
val hashCode = hashCode()
println("length = $length, isEmpty = $isEmpty, hashCode = $hashCode")
}
fun main() {
val language = "kotlin"
language.info()
}
// 输出
length = 6, isEmpty = false, hashCode = -1125574399
在上述示例代码中,我们给String类定义了一个“扩展函数类型”的属性info,来输出当前字符串的一些信息。并使用Lambda表达式对其进行了初始化。在花括号中我们可以直接使用this来访问接受者类中的属性和方法。
无疑声明一个带接受者的函数类型,在对函数类型初始化的Lambda表达式中,我们将拥有当前接受者的上下文。可以在Lambda表达式的{ }中像在接受者类内部一样,访问该接受者任意公开的方法和属性。
7.拥有函数返回值的函数
其实拥有函数类型返回值的函数,笔者在实际的开发中自己也是很少用到,这听起来也比较抽象。但是官网对高阶函数的定义有说到这点,这里还是介绍一下比较好。下面我们来声明一个返回值是函数类型的函数sum:
fun sum(): (Int, Int) -> Int {
val block = { left: Int, right: Int -> left + right }
return block
}
fun main() {
val left = 101
val right = 102
val result = sum().invoke(left, right)
println("result = $result")
}
// 输出
result = 203
我们在main函数中访问sum函数时其返回的只是一个函数类型:(Int, Int) -> Int的实例,也就是一个“匿名Lambda表达式”。而我们要执行函数类型中函数体的代码,就需要对这个函数类型的实例再进行一次调用。于是就有了上面的写法:
val result = sum().invoke(left, right)
// 或者
val result = sum()(left, right)
为了提高代码的可读性,上面的代码刻意的用了funName.invoke的方式进行了调用。 其实官方的源码中Suspend.kt的文件中也给我们提供了一个拥有函数返回值的函数suspend:
public inline fun <R> suspend(noinline block: suspend () -> R): suspend () -> R = block
关于suspend关键字修饰的函数或者函数类型的参数我们称之为挂起函数。在后面介绍协程的文章中我们再详细的介绍。
8.函数式接口
只有一个抽象方法的接口我们称之为函数式接口,或者说是单一抽象方法接口。例如我们最常见的OnClickListener接口就是一个函数式接口。
public interface OnClickListener {
void onClick(View v);
}
在Java中当我们要给一个View设置点击事件的时候,通常我们会用new
关键字创建一个匿名类的方式来实现:
view.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
}
});
而在Kotlin中创建一个匿名类我们使用关键字object
view.setOnClickListener(object :View.OnClickListener {
override fun onClick(v: View?) {
}
})
这种方式和Java中给一个View设置点击事件在写法上看不出任何的优势。
但是在Kotlin中对于这种具有函数式的接口的调用,我们可以对等的转换成对Lambda表达式的调用。这可能不太好理解,结合一下函数类型和函数类型的初始化,我们来分解一下这个问题。我们把在Java中定义的一个接口,等价的转换成在Kotlin中定义的一个函数类型,如上面的OnClicklistener接口对等的转换成函数类型就是 (View)-> Unit。
// Java接口
public interface OnClickListener {
void onClick(View v);
}
// Kotlin函数类型
(View) -> Unit
对接口的匿名类实现的方式我们对等的转换成函数类型初始化的方式。
// Java匿名类
new View.OnClickListener() {
@Override
public void onClick(View v) {
}
}
// Kotlin Lambda表达式
{ view: View -> }
现在我们再使用Lambda表达式的方式来简化上面对View点击事件的设置:
view.setOnClickListener({ view: View -> println("view = $view") })
Lambda表达式是该方法唯一参数,可以在调用时省略 (),将花括号 { } 直接放在方法名后。
view.setOnClickListener { view: View -> println("view = $view") }
函数类型只有一个参数,可以在Lambda表达式中省略参数的声明和 ->。该参数将被隐式的声明为it。
view.setOnClickListener { println("view = $it") }
事实上,Lambda表达式在转换成Java代码的时候也确实是使用匿名类的方式实现的。
9.实现我们自己的let方法
有了高阶函数和Lambda表达式的基础,我们就可以自己来实现标准库Standard.kt中的一些高阶函数了。下面就让我们实现一个自己的let函数吧!
首先我们来分析一下我们平时在使用let函数的一些特性。它可以让任意对象在任意的地方调用。那么我们可以分析出let函数函数首先一定是一个泛型函数且是一个扩展函数,而它又可以在任意地方调用,那么let函数一定是一个顶层函数。初步推断出let函数张这样:
fun <T> T.let() {
}
当我们在调用let函数的时候,通常我们都是使用Lambda的方式。这说明let函数拥有一个函数类型的参数。
fun <T> T.let(block: () -> Unit) {
}
而在Lambda表达式中我们又可以获取到调用者的隐式名称it。那么let函数中的函数类型参数,一定拥有一个自己的参数,且这个参数是调用者自己,我们需要在let函数内部将当前调用者的引用传递给函数类型的参数block。而这个this,就是我们在调用let函数时Lambda表达式中的隐式引用it。
fun <T> T.let(block: (T) -> Unit) {
block(this)
}
关于标准库Standard.kt中提供的很多标准函数,我们只要掌握了函数类型和Lambda表达式的语法。都可以在自己分析后,然后很熟练的写出。这就当做是我们的课后习题吧。
自己动手实现apply、also、with函数。
总结
函数类型和Lambda表达式在Kotlin语言的开发中可以说是无处不在。完全的掌握和理解它,对我们在实际开发过程中会有很大的帮助。
好了,这篇文章到这里就结束了。下篇文章我们将介绍Kotlin中inline、noinline和crossinline关键字的使用。我们下期再见!