匿名函数
Lambda表达式虽然简洁、方便,但它有一个严重的缺陷: Lambda 表达式不能指定返回值类型。
大部分时候,由于Kotlin可以推断出Lambda表达式的返回值类型,因此即使不为Lambda 表达式指定返回值类型也没有问题。但在一些特殊的场景下,如果 Kotlin无法推断出 Lambda 表达式的返回值类型,此时就需要显式指定返回值类型,而匿名函数即可代替 Lambda表达式。
匿名函数的用法
fun main(args: Array<String>) {
//定义匿名函数, 赋值给test变量
var test = fun(x:Int,y:Int):Int{
return x+y
}
//通过 test 调用匿名函数
println(test(2,4))
}
从上面代码可以看出,匿名函数与普通函数基本相似,只要将普通函数的函数名去掉就变成了匿名函数 。与普通函数不同的是,如果系统可以推断出匿名函数的形参类型,那么匿名函数允许省略形参类型 。
fun main(args: Array<String>) {
var filteredList = listOf<Int>(3, 5, 7, 9).filter(
//使用匿名函数作为 filter ()方法的参数
fun(e1): Boolean {
return Math.abs(e1) > 5
}
)
//[7, 9]
println(filteredList)
}
上面程序调用 List 的 filter()方法时需要传入一个(lnt)->Boolean 类型的参数,此时既可传入一个 Lambda 表达式,也可传入一个匿名函数,此处我们特意传入一个匿名函数作为参数。由于系统完全可以推断出该参数的类型必须是(Int)->Boolean,因此此处允许省略匿名函数的形参类型 。
匿名函数的返回值类型的声明规则与普通函数相同:如果使用普通代码块作为函数体,则匿名函数需要显式指定返回值类型,否则认为该匿名函数没有返回值(相当于Unit);如果使用单表达式作为函数体,则无须指定返回值类型,系统可以自动推断出 。
fun main(args: Array<String>) {
//定义匿名函数的函数体是单表达式,可以省略声明函数的返回值类型
var intValue = fun(x: Int, y: Int) = x + y
//通过intValue调用匿名函数
println(intValue(4, 5))
var rt = listOf<Int>(9, 7, 5, 3, 1).filter(
// 使用匿名函数作为 filter ()方法的参数
// 匿名函数的函数体是单表达式,允许省略声明函数的返回值类型
fun(y) = y > 5
)
//[9, 7]
println(rt)
}
匿名函数和 Lambda 表达式的 return
匿名函数的本质依然是函数,因此匿名函数的 return 则用于返回该函数本身;而 Lambda表达式的 return用于返回它所在的函数,而不是返回 Lambda 表达式 。
fun main(args: Array<String>) {
var list = listOf<Int>(9, 7, 5, 3, 1)
//使用医名函数执行 forEach ()方法
list.forEach(fun(n) {
println("元素依次为: ${n}")
// 匿名函数中的 return 用于返回该函数本身
return
})
//使用 Lambda 表达式执行 forEach ()方法
list.forEach({ n ->
println("元素依次为:${n}")
//Lambda 表达式中的 return 用于返回它所在的函数( main 函数)
return
})
}
上面代码之所以可以在 Lambda表达式中直接使用 return,是因为该 forEach方法使用了 inline修饰,因此它相当于一个内联函数。
上面程序中分别使用了两种方式来调用 List 集合的 forEach()方法:第一种方式是使用匿名函数,匿名函数中的 return 用于返回该函数本身,因此对 main()函数没有任何影响,故这种方式可以正常遍历整个 List集合;第二种方式是使用 Lambda表达式,该表达式中的retunrn用于返回它所在的函数( main 函数),因此该forEach()方法只能输出一个数组元素 。
如果真的很想过一下在 Lambda 表达式中使用return的瘾,则可使用限定返回的语法。
fun main(args: Array<String>) {
var list = listOf<Int>(9, 7, 5, 3, 1)
//使用 Lambda 表达式执行 forEach ()方法
list.forEach({ n ->
println("元素依次为:${n}")
//使用限定返回,此时 return 只是返回传给 forEach 方法的 Lambda 表达式
return@forEach
})
}
捕获上下文中的变量和常量
Lambda 表达式或匿名函数(以及局部函数、对象表达式)可以访问或修改其所在上下文(俗称“闭包”)中的变量和常量,这个过程被称为捕获。即使定义这些变量和常量的作用域已经不存在了,Lambda 表达式或匿名函数也依然可以访问或修改它们。
例如,如下程序先定义了一个函数,然后在该函数内定义了一个局部函数,此时局部函数就可以访问或修改其所在上下文(函数)中的变量。
//定义一个函数,该函数的返回值类型为()-> List<String>
fun makeList(ele: String): () -> List<String> {
//创建一个不包含任何元索的 List
var list: MutableList<String> = mutableListOf<String>()
fun addElement(): List<String> {
//向 list集合中添加一个元素
list.add(ele)
return list
}
return ::addElement
}
内联函数
先简单介绍一下高阶函数(为函数传入函数或 Lambda 表达式作为参数)的调用过程。调用Lambda表达式或函数的过程是: 程序要将执行顺序转移到被调用表达式或函数所在的内存地址,当被调用表达式或函数执行完后,再返回到原函数执行的地方。
在上面这个转移过程中,系统要处理如下事情。
- 为被调用的表达式或函数创建 一个对象。
- 被调用的表达式或函数所捕获的变量创建一个副本。
- 在跳转到被调用的表达式或函数所在的地址之前,要先保护现场并记录执行地址:从被调用的表达式或函数地址返回时,要先恢复现场,并按原来保存的地址继续执行。也就是通常所说的压栈和出栈。
从上面介绍不难看出,函数调用会产生一定的时间和空间开销,如果被调用的表达式或函数的代码量本身不大,而且该表达式或函数经常被调用,那么这个时间和空间开销的损耗就很不划算。
为了避免产生函数调用的过程,我们可以考虑直接把被调用的表达式或函数的代码“嵌入” 原来的执行流中,简单来说,就是编译器负责去“复制、粘贴”: 复制被调用的表达式或函数的代码,然后粘贴到原来的执行代码中。为了让编译器帮我们干这个复制、粘贴的活,可通过内联函数来实现。
内联函数的使用
使用内联函数非常简单,只要使用 inline关键字修饰带函数形参的函数即可。下面程序示范了内联函数和非内联函数的区别。
inline fun map2(data: Array<Int>, fn: (Int) -> Int): Array<Int> {
var result = Array<Int>(data.size,{0})
//遍历 data 数组的每个元素,并用 fn 函数对 data[i]进行计算
// 然后将计算结果作为新数组的元素
for (i in data.indices){
result[i] = fn(data[i])
}
return result
}
fun main(args: Array<String>) {
var arr = arrayOf(100,40,60,120,70,90)
var mappedResultl = map2(arr,{it*3})
println(mappedResultl.contentToString())
}
上面 map2()函数包含一个函数类型的形参,且该函数使用了 inline修饰,因此它是一个内联函数。编译该程序,会发现编译结果只产生一个 InlineTestKt.class文件,不会生成其他额外的内部类的 class 文件,这表明编译器实际上会将 Lambda 表达式的代码复制、粘贴到 map() 函数中。
也就是说,程序调用的 map 函数编译之后实际上变成了如下形式:
fun map2(data: Array<Int>, fn: (Int) -> Int): Array<Int> {
var result = Array<Int>(data.size,{0})
//遍历 data 数组的每个元素,并用 fn 函数对 data[i]进行计算
// 然后将计算结果作为新数组的元素
for (i in data.indices){
result[i] = data[i]*3
}
return result
}
从 result[i] = data[i]*3 看来此时根本就不存在函数调用,自然也就不需要额外生成函数对象了,也不会产生捕获,也不需要处理函数调用的压栈和出栈开销 。
如果去掉 InlineTest.kt程序中 map2函数的 inline修饰符,再次编译该函数,将会看到系统生成 InlineTestKt.class 和 InlineTestKt$main$mappedResult1$1.class 文件,这表明系统将会为
Lambda表达式额外生成一个函数对象,自然也会产生函数调用的压栈和出栈开销 。
介绍到此处,可能有读者会想,既然内联函数这么好,那么我们干脆全部使用内联函数好了。下面的问题是:内联函数的缺点在哪里呢?到底哪些情况应该使用内联函数,哪些情况不应该使用内联函数呢?
通过前面的介绍我们知道,内联函数的本质是将被调用的 Lambda表达式或函数的代码复制、粘贴到原来的执行函数中,因此如果被调用的 Lambda表达式或函数的代码量非常大,且该Lambda 表达式或函数多次被调用,注意每调用一次,该 Lambda 表达式或函数的代码就会被复制一次 , 因此势必带来程序代码量的急剧增加。
由此可见,内联函数并不总是带来好处的,内联函数是以目标代码的增加为代价来节省时间开销的。因此是否应该使用内联函数的答案是:如果被调用的 Lambda 表达式或函数包含大量的执行代码,那么就不应该使用内联函数:如果被调用的 Lambda表达式或函数只包含非常简单的执行代码(尤其是单表达式),那么就应该使用内联函数。
部分禁止内联
使用 inline修饰函数之后,所有传入该函数的 Lambda表达式或函数都会被内联化;如果我们又希望该函数中某一个或某几个函数类型的形参不会被内联化,则可考虑使用 noinline修饰它们。
简单来说, noinline用于显式阻止某一个或某几个形参内联化。
inline fun test1(test2:(Int)->Int, noinline test3:(String)->String){
println(test2(111))
println(test3("222"))
}
上面程序中的代码使用 inline修饰了 test1()函数,该函数带有两个函数类型的形参, 因此在默认情况下,所有传入test1()函数的 Lambda 表达式或函数都会被内联化,如果没有为后面第二个 test3形参使用 noinline修饰,编译该程序将只会看到一个 NolnlineTest.class文件, 这意味着传给该函数的两个表达式都被直接复制到 test1()函数中。
如果为 test1()函数的test3 形参添加了 noinline 修饰符,再次编译该文件将会看到系统生成NolnlineTestKt.class 和 NolnlineTestKt$main$1.class 两个文件,这就意味着 Kotlin 取消了调用 test1()函数所传入的第二个 Lambda 表达式的内联化,因此编译时只需为传入 test1()函数的第一个 Lambda表达式额外生成一个函数对象。
非局部返回
前面已经提到,在Lambda 表达式中直接使用return不是用于返回该表达式,而是用于返回该表达式所在的函数。但要记住在 默认情况下,在 Lambda 表达式中并不允许直接使用return。这是因为如果是非内联的 Lambda表达式,该 Lambda表达式会额外生成一个函数对象, 因此这种表达式中的 return不可能用于返回它所在的函数 。
由于内联的 Lambda 表达式会被直接复制、粘贴到调用它的函数中,故此时在该Lambda 表达式中可以使用return,该return就像直接写在 Lambda表达式的调用函数中一样。 因此该内联的 Lambda表达式中的return可用于返回它所在的函数,这种返回被称为非局部返回 。
//定义函数类型的形参 ,其中 fn1 是( Int)-> Unit 类型的形参
inline fun each(data: Array<Int>, fn1: (Int) -> Unit) {
for (el in data) {
fn1(el)
}
}
fun main(args: Array<String>) {
var arr = arrayOf(40, 90, 10, 50, 40)
each(arr, {
println(it)
return //如果 each 函数没有 inline 修饰,此处编译错误
//如果 each 函数有 inline 修饰,此处的 return 将返回 main 函数
})
}
另外,某些内联函数不是直接从函数体中调用 Lambda表达式的,而是从其他的执行上下文(如局部对象或局部函数)中来获取 Lambda表达式的。在这种情况下,非局部返回的控制流也不允许出现在 Lambda表达式中。此时应该使用 crossinline来修饰该参数。









网友评论