简介
高阶函数是将函数用作参数或返回值的函数。在Kotlin函数都是头等的,可以将存储在变量与数据结构中、作为参数传递给其他函数或者将它们作为返回值从其他函数里返回。我们不仅可以自己定义高阶函数。
同时Kotlin还为我们提供了大量的内联高阶函数如let、run、also、with等。这些函数都是基于高阶函数实现的,掌握了高阶函数后,才能真正的了解和明白这些内置函数。
下面就是一个高阶函数的简单的使用案例(大家在实际开发中一定要注意代码规范,不要像我这样偷懒而去随便命名):
fun main() {
val fun1 = fun(name: String) {
println(name)
}
//高阶函数使用
func2("白瑞德", fun1)
//lambda表达式写法
func2("白瑞德") {
println(it)
}
}
fun func2(name: String, func: (String) -> Unit) {
func("名字叫做$name")
}
因为在Kotlin中,如果在默认参数之后的最后一个参数是lambda表达式,那么它既可以作为具名参数在括号内传入,也可以在括号外传入。所以才有了第二种写法。接下来我们都会使用lambda的方式书写代码。
高阶函数形参以及返回值的定义
Kotlin中的函数语法一般如下:
修饰符 函数名(参数名:参数类型) : 返回值类型 {
...
方法体
...
return 返回值;
}
按照简介里的定义,如果想定义一个高阶函数,只需要将参数类型
或返回值类型
中的任意一个定义为函数类型就可以了。我们都知道函数的三个重要的构成:函数名、形参和返回值。要将函数作为参数,那么形参就可以指代它了。所以无需在函数名上耗费太多精力。只需要关注形参和返回值就好了。例如上文中的func2
函数的形参func: (String) -> Unit
。它的意思就是接收一个入参为String类型,没有返回值(void)的函数。
有了这个理解,我们就可以很明确的确定形参和返回值的定义格式了:func:(T...)->R
。其中T和R就是你想要的具体类型(在Kotlin内置的高阶函数如let,run,also等就是使用T、R来表示泛型)。T...表示可以有多个形参。而R也可以是Unit, Unit类型实现了与Java中的void一样的功能,表示函数无需返回值。
下面我们将分类展示一些它们的使用以及一些区别:
- ()->R
- (T...)->R
- T.()->R
- 组合类T.(Any)->R
我们首先创建一个User类供我们演示使用:
class User {
var name: String = ""
var age: Int = 0
constructor(name: String, age: Int) {
this.name = name
this.age = age
}
fun printMessage() {
println("姓名:$name+年龄:$age")
}
}
代码很简单,两个成员变量和一个打印成员变量的函数。
首先看第一组:
()->R
这种形式的参数接收一个返回值为R的无参函数,如果R是Unit类型,则没有返回值。
首先定义一个高阶函数:
fun uFun1(funcUnit:()->Unit,funcUser:()->User){
funcUnit()
funcUser().printMessage()
}
uFun1接受两个无参的函数,唯一的区别是后者会返回一个User实例。接下来我们看一下使用方式:
fun main() {
val u1 = fun() {
println("要准备打印个人信息了")
}
val u2 = fun(): User {
return User("白瑞德", 19)
}
uFun1(u1, u2)
uFun1(u1) {
User("白瑞德", 19)
}
}
输出结果如下:
要准备打印个人信息了
姓名:白瑞德+年龄:19
要准备打印个人信息了
姓名:白瑞德+年龄:19
注意看lambda的实现方式,连return关键字都省略了。
我们通过编译器将Kotlin的字节码实现转换成Java代码,看一下它的运行机制:
不难看出,在Kotlin中原本为函数的参数在Java中被一个Function0对象替代了。
而Function0是一个接口,具体实现如下:
看到这里恍然大悟:Kotlin中的高阶函数中,作为参数的函数,实际上再JVM中就是使用匿名内部类实现的。
(T...)->R
这种形式的参数接收一个返回值为R且有参数T(数量不固定,类型也不固定)函数,如果R是Unit类型,则没有返回值。
首先定义一个高阶函数:
fun uFun2(funcUnit: (user: User) -> Unit, funcUser: (user: User, afterYears: Int) -> String) {
val user = User("白瑞德", 18)
funcUnit(user)
val funcUser1 = funcUser(user, 10)
println(funcUser1)
}
它接收两个函数:第一个形参为User无返回值;第二个形参有两个User和int,返回值为String。
使用方法如下:
fun main() {
val u3 = fun(user: User) {
user.printMessage()
}
val u4 = fun(user: User, afterYears: Int): String {
user.age += afterYears
return "${user.name}今年${user.age - afterYears}岁"
}
val u5 = fun User.() {
printMessage()
}
uFun2(u3, u4)
uFun2(u5) { u, y -> "${u.name}在$y 年后就是${u.age + y}岁了" }
uFun3 {
this.name
}
}
和()->R一样,lambda的实现方式明显更加简介高效。
代码里的u5处所定义的函数需要注意,它是一个User的扩展函数。
fun User.() 和 fun(user: User)的使用方法相同,他们都需要传入一个User对象。区别在于函数体内对User对象的调用方式不同。前者既然作为扩展函数,可以直接向在User类内部调用函数一样,直接使用类所定义的成员变量和函数(实际是通过this的方式访问的,但是this可省略)。后者就是一个普通的函数了,如果想要访问User类内的任意函数和变量,都必须使用user.
的方式显式的调用。另外它们还有一点共同点,就是他们都不支持对User的私有成员进行访问。
T.()->R
这种方式和 (T...)->R的要求基本类似,只是对类的成员访问的方式不同。
看一下具体实现:
fun uFun3(funcUnit: User.() -> Unit, funcUser: User.(afterYears: Int) -> String) {
val user = User("白瑞德", 18)
val funcUnit = funcUnit(user)
val funcString = user.funcUser(10)
println(funcUnit)
println(funcString)
}
对于User.()类型的形参,不仅可以使用funcUnit(user)的方式访问,也可以使用 user. funcUnit()的形式访问。 而在实参函数体里的情形,已经在 (T...)->R小节里讲述过了。这里不再累述。
返回值为函数的情况
返回值为函数的情况下,对于返回值的定义规范和形参一样:
fun uFun4():() -> Unit{
return fun(){
println(User("白瑞德", 18).name)
}
}
使用方法如下:
uFun4()()
uFun4().invoke()
将字节码实现转换为Java后代码如下:
也是使用匿名内部类的方式实现的。
Kotlin内置高阶函数
kotlin为我们提供了很多内置高阶函数,如let、run、also、with等。都是以内联函数的形式定义在Standard.kt文件中。
相信大家看一下这些方法接收的参数,不难发现它们的参数类型基本上就是我们在上文中提到的。由此我们变不难掌握let、run、also、with的区别和用法。
-
() -> R:形参要求是无参数的方法,可返回任意类型。而且无需显式的返回,默认返回值为最后一行的变量或表达式;如:
val r: String = run { "" } val b: Boolean = run { 1==2 }
-
(T)->R:形参要求是一个形参为T的方法,返回值为R类型。同样无需显式返回,默认返回最会一行的变量或表达式。并且在函数体内需要使用
it.
的方式来访问参数T实例的成员;其中(T)->Unit、(T)->Int和(T)->Boolean属于(T)->R,只是它们指明了具体的返回值类型而不是一个泛型。如:val letInt:Int = str.let { it.length } val takeString:String? = str.takeIf { it.contains("l") }
-
T.()->R:形参要求是一个形参为T的方法,返回值为R类型。同样无需显式返回,默认返回最会一行的变量或表达式。如果R为Unit类型,则返回的为调用者T。否则和(T)->R一样,无需显式返回,默认返回最会一行的变量或表达式。但是(T)->R不同的是它无需显式的指明T,即可直接访问T的成员,就像在T的类里面访问一样(不包括私有成员),和如:
val strAlso:String = str.apply { println(hashCode()) } val with:Int = with(str) { println(length) 0 }
另外,with和report都的参数是(T,T.() -> R)的样式。意思是接收一个类型为 T 的参数和一个代码块。但实际使用除了调动方式不同外,基本没有什么异样。
最后,关于这些内置函数的返回值,它们的返回值类型取决于内联函数的返回值。这里仅看函数定义的返回值还不够。
public inline fun <T> T.apply(block: T.() -> Unit): T {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
block()
return this
}
public inline fun <T, R> T.let(block: (T) -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return block(this)
}
以apply和let为例:apply的返回值一个为this,也就是调用者本身;let的返回值会传入的函数的返回值。也就是block返回什么(代码块最后一行),它就返回什么:这就是这些函数有的返回调用者本身,有的返回传入的代码块的最后一行的原因。
总结这些内置的函数异同如下:
函数名 | 函数块内的对象 | 返回值 |
---|---|---|
let | it | 最后一行 |
with | this(无需显式调用) | 最后一行 |
run | this(无需显式调用) | 最后一行 |
also | it | 调用者本身 |
apply | this | 调用者本身 |
无需死记硬背它们的区别。如果弄懂了函数作为参数的,你很快就会理解这些内置函数的特点和作用。即使一时想不起,看一下它们的定义就一切都明了了。如果实在还是弄不懂,可以看更入门的文章# kotlin Standard中的内置内联高阶函数
inline和noinline
不知道你有没有发现我一直称let、run、also、with为内联高阶函数。为什么会加个内联呢?因为它们的函数定义如下:
public inline fun <R> run(block: () -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return block()
}
多了一个inline
关键字。它有什么用呢?不妨让我们对比下下面的代码:
run {
println("-----")
}
val func = fun() {
println("-----")
}()
它们运行的效果一样。实现的功能也一样,但是,它们的字节码实现却大不相同。转换为Java实现如下:
明明上文中才说,高阶函数是通过匿名内部类实现的吗?这里怎么只有一个匿名内部类?
没错,这就是inline的作用。它不再是使用匿名内部类的方式,而是相当于直接把代码块"粘贴"到当前位置。这样可以通过内联的形式进行编译,减少对象的创建提高性能。
理解了inline,自然就能才到noinline的含义了:不要内联。可是,函数不都是默认不内联的吗?和inline不同的是,noinline不是作用于函数的,而是作用在函数的参数上的。那么它有什么效果呢——就是字面意思,关闭内联。???那直接不用inline不就好了吗?但是,思考一个问题,如果我有一个高阶函数,它的形参和返回值都是函数类型:
fun lineFun(num:Int,func: () -> Int): () -> Int {
println("--------$num")
return func
}
而此时,要将lineFun定义为内联函数:
很不幸,编译器报错了:func是非法的内联参数,需要添加nolinline去修饰它。这又是为什么呢?
还记得我们提过,内联函数并不是借助匿名内部类实现的。它更像是直接把代码粘贴过的,而且它内部的参数也不再是对象了。假如lineFun是个内联函数,我们这样调用它:
println("-----1")
val func = fun(): Int {
println("-----2")
return 0
}
lineFun(1, func)()
那么按照内联规则,编译后应该是下面的样子:
println("-----1")
println("-----2")
println("--------$1")
return ?????
没错,函数实参func也被内联了,那么它所要做就是将自己的函数体展开,也就是只需要贡献代码块并将它粘贴到调用出即可,那么它也不再是个对象了,那么返回值怎么办呢?这时候就要请出noinline了。当func被noinline标记后,它就不再参与内联,而是使用匿名内部类的方式参与其中。自然也就结局返回的问题了。