函数和Lambda表达式
函数是代码复用的重要手段,Kotlin 支持局部函数(实际上局部函数是Lambda 表达式的基础),Lambda 表达式可作为表达式、函数参数或函数返回值
函数入门
定义和调用函数
定义函数时必须为形参指定类型,调用函数时必须为形参传入参数值,而且传入的参数值必须与形参类型保持一致,如果为函数声明了返回值类型,则应该在函数体中使用 return 显式返回一个值
//语法格式,如果希望声明函数没有返回值
//省略“:返回值类型”部分,或者使用"Unit"指定没有返回值
fun 函数名(形参列表)[:返回值类型]{
//函数语句
}
fun max(num1: Int, num2: Int): Int {
var max = if (num1 > num2) num1 else num2
return max
}
递归函数
在一个函数体内调用它自身,被称为函数递归。如:
已知一个数列:f(0)=1,f(1)=4,f(n+2)=2*f(n+1)+f(n),求f(10),就可以使用递归函数
fun fn(n: Int): Int {
return when (n) {
0 -> 1
1 -> 4
//在函数中调用自身,就是递归函数
else -> 2 * fn(n - 1) + fn(n - 2)
}
}
函数递归必须在某个时刻函数的返回值是确定的,即不再调用它自身,递归一定要向己知方向进行
单表达式函数
在某些情况下,函数只是返回单个表达式,编译器可以推断出函数的返回值类型,此时可以省略返回值类型、花括号并在等号(=)后指定函数体即可
fun area(width: Double, height: Double) = width * height
函数的形参
命名参数
Kotlin函数的参数名不是无意义的, Kotlin 可以调用函数时通过名字来传入参数值
fun area(width: Double, height: Double) = width * height
//......
//传统调用函数的方式,根据位置传入参数
area(4.4, 6.2)
//根据参数名来传入参数
area(width = 3.5, height = 9.8)
//使用命名参数时可交换位置
area(height = 9.8, width = 3.5)
//部分使用命名参数,部分使用位置参数
area(4.6, height = 9.8)
如果希望调用函数时混合使用命名参数和位置参数,那么命名参数必须位于位置参数之后
形参默认值
在某些情况下,程序需要在定义函数时为一个或多个形参指定默认值,这样调用函数时就可以省略该形参,通过为函数形参指定默认值,可以减少函数重载的数量,通常建议将带默认值的参数定义在形参列表的最后
fun area(width: Double = 1.0, height: Double = 1.0) = width * height
//......
area()
area(4.2)
area(4.2, 55.6)
area(height = 5.7)
尾递归函数
当函数将调用自身作为它执行的最后一行代码,且递归调用后没有更多代码时,可使用尾递归语法,尾递归不能在异常处理的try、catch、finally 块中使用,需要使用 tailrec 修饰
/**
* 计算阶乘的函数
* @param n Int
* @return Int
*/
fun fact(n: Int): Int {
if (n == 1) {
return 1
} else {
return n * (fact(n - 1))
}
}
/**
* 使用尾递归函数的语法
* @param n Int
* @param total Int
* @return Int
*/
tailrec fun factRec(n: Int, total: Int = 1): Int =
if (n == 1) total else factRec(n - 1, total * n)
编译器会对尾递归进行修改,将其优化成一个快速而高效的基于循环的版本,从而避免栈溢出,这样可以减少可能对内存的消耗
个数可变的形参
如果在定义函数时,在形参的类型前添加 vararg修饰,则表明该形参可以接受多个参数值,多个参数值被当成数组传入,允许个数可变的形参可以处于形参列表的任意位置(不要求是形参列表的最后参数),但要求一个函数最多只能带一个个数可变的形参
fun test(vararg books: String, num: Int) {
books.forEach { Log.d(TAG, "test: $it") }
Log.d(TAG, "test: $num")
}
如果我们已有一个数组,希望将数组的多个元素传给个数可变的参数,则可以在传入的数组参数前添加“*”运算符
var array = arrayOf("android", "kotlin")
test(*array, num = 2)
函数重载
kotlin 许定义多个同名函数,只要形参列表不同就行,包括形参个数不同、形参类型不同,这被称为函数重载
//方法一
fun test() {
Log.d(TAG, "test: 无参数的test函数")
}
//方法二
fun test(msg: String) {
Log.d(TAG, "test: 该函数类型为(String): Unit")
}
//方法三
fun test(num: Int): String {
Log.d(TAG, "test: 该函数类型为(Int): String")
return "test"
}
//方法四
fun test(vararg books: String, num: Int): String {
Log.d(TAG, "test: 该函数类型为(Int): String")
return "test"
}
//......
test()
test("kotlin")
test(3) //调用的是方法三
test("kotlin", "linux", num = 5)
test(3)虽然既可匹配方法三,也可匹配方法四,但 Kotlin 会尽量执行最精确的匹配
局部函数
Kotlin 支持在函数体内部定义函数,这种被放在函数体内定义的函数称为局部函数,默认情况下,局部函数对外部是隐藏的,局部函数只能在其封闭函数内有效,其封闭函数也可以返回局部函数,以便程序在其他作用域中使用局部函数
fun getMathFunc(type: String, nn: Int): Int {
//定义一个计算平方的局部函数
fun square(n: Int): Int {
return n * n
}
//定义一个计算立方的用部函数
fun cube(n: Int): Int {
return n * n * n
}
//定义一个计算阶乘的局部函数
fun factorial(n: Int): Int {
var result = 1
for (index in 2..n) {
result *= index
}
return result
}
return when (type) {
//调用局部函数
"square" -> square(nn)
"cube" -> cube(nn)
else -> factorial(nn)
}
}
高阶函数
函数本身也具有自己的类型,函数类型就像前面介绍的数据类型一样,既可用于定义变量,也可用作函数的形参类型,还可作为函数的返回值类型
如果一个函数接收另一个函数作为参数,或者返回值的类型是另一个函数,那么该函数就称为高阶函数
使用函数类型
Kotlin 的每个函数都有特定的类型,函数类型由函数的形参列表、->和返回值类型组成,例如如下函数:
//方法一:无参数的test函数,函数类型为()->Unit或者()
fun test() {
Log.d(TAG, "test: 无参数的test函数")
}
//方法二:该函数类型为(String)->Unit或者(String)
fun test(msg: String) {
Log.d(TAG, "test: 该函数类型为(String): Unit")
}
//方法三:该函数类型为(Int, Int)->String
fun test(num1: Int, num2: Int): String {
Log.d(TAG, "test: 该函数类型为(Int): String")
return "test"
}
- 使用函数类型定义变量:
//定义一个变量,其类型为(Int , Int) -> Int
var myfun: (Int, Int) -> Int
//定义一个变量,其类型为(String)
var test : (String)
- 函数赋值给该变量:
fun area(width: Int, height: Int) = width * height
......
//当直接访问一个函数的函数引用,而不是调用函数时,需要在函数名前添加两个冒号,
//而且不能在函数后面添加圆括号
myfun = ::area
- 使用函数类型变量
var areaValue = myfun(4, 6)
使用函数类型作为形参类型
有时候需要定义一个函数,如果希望调用函数时能动态传入某些代码,就需要在函数中定义函数类型的形参,这样可在调用该函数时传入不同的函数作为参数,从而动态改变这些代码
//定义一个计算平方的局部函数
private fun square(n: Int): Int {
return n * n
}
//定义一个计算立方的用部函数
private fun cube(n: Int): Int {
return n * n * n
}
//定义函数类型的形参,其中 fn 是(Int)->Int 类型的形参
private fun map(data: Array<Int>, fn: (Int) -> Int): Array<Int> {
var result = Array(data.size) { 0 }
//遍历 data 数组的每个元素,并用 fn 函数对 data[i]进行计算
//然后将计算结果作为新数组的元素
for (i in data.indices) {
result[i] = fn(data[i])
}
return result
}
......
//使用函数类型的形参
var array = arrayOf(4, 6, 2, 8)
Log.d(TAG, "onCreate: 计算平方 ${map(array, ::square)}")
Log.d(TAG, "onCreate: 计算立方 ${map(array, ::cube)}")
使用函数类型作为返回值类型
fun getMathFunc(type: String): (Int) -> Int {
//定义一个计算平方的局部函数
fun square(n: Int): Int {
return n * n
}
//定义一个计算立方的用部函数
fun cube(n: Int): Int {
return n * n * n
}
//定义一个计算阶乘的局部函数
fun factorial(n: Int): Int {
var result = 1
for (index in 2..n) {
result *= index
}
return result
}
//返回局部函数
return when (type) {
//调用局部函数
"square" -> ::square
"cube" -> ::cube
else -> ::factorial
}
}
......
//得到函数类型的返回值,并进行调用
var cube = getMathFunc("cube")
Log.d(TAG, "onCreate: cube ${cube(6)}")
var square = getMathFunc("square")
Log.d(TAG, "onCreate: square ${square(6)}")
var other = getMathFunc("other")
Log.d(TAG, "onCreate: other ${other(6)}")
局部函数与 Lambda 表达式
使用 Lambda 表达式代替局部函数
像上面的代码,局部函数的函数名仅仅在 when 表达式中作为返回值使用,一旦离开了getMathFunc()函数体,局部函数的函数名就失去了意义,那么就考虑使用 Lambda 表达式来简化局部函数的写法
//该函数的返回值类型为(Int) -> Int
fun getMathFunc(type: String): (Int) -> Int {
//该函数返回的是 Lambda 表达式
return when (type) {
"square" -> return { n ->
n * n
}
"cube" -> return { n ->
n * n * n
}
else -> return { n ->
var result = 1
for (index in 2..n) {
result *= index
}
result
}
}
}
Lambda 表达式的标准语法如下
{ (形参列表) ->
//零条到多条可执行语句
}
Lambda 表达式与局部函数只是存在如下区别:
- Lambda 表达式总是被大括号括着
- 定义 Lambda 表达式不需要 fun 关键字,无须指定函数名
- 形参列表(如果有的话)在->之前声明,参数类型可以省略
- 函数体(Lambda 表达式执行体)放在->之后
- 函数的最后一个表达式自动被作为 Lambda 表达式的返回值,无须使用 return 关键字
Lambda 表达式的脱离
作为函数参数传入的 Lambda 表达式可以脱离函数独立使用
//定义一个 List 类型的变量,并将其初始化为空 List
var lambdaList = ArrayList<(Int) -> Int>()
//定义一个函数,该函数的形参类型为函数
fun collectFn(fn: (Int) -> Int) {
//将传入的 fn 参数(函数或 Lambda 表达式)添加到 lambdaList 集合中
//这意味着 fn 将可以在 collectFn 范围之外使用
lambdaList.add(fn)
}
//......
//使用
//向 lambdaList 中添加元素
collectFn { n -> n * n }
collectFn { n -> n * n * n }
//依次调用 lambdaList 集合的元素(每个元素都是 Lambda 表达式)
lambdaList.forEach { method ->
Log.d(TAG, "onCreate: ${method(10)}")
}
Lambda 表达式
调用 Lambda 表达式
Lambda 表达式的本质是功能更灵活的代码块,因此完全可以将 Lambda 表达式赋值给变量或直接调用 Lambda 表达式
//定义一个 Lambda 表达式,并将它赋值给 square
var square = { n: Int ->
n * n
}
//使用 square 调用 Lambda 表达式
Log.d(TAG, "onCreate: ${square(5)}")
//定义一个 Lambda 表达式,并在它后面添加圆括号来调用该 Lambda 表达式
var max = { num1: Int, num2: Int ->
if (num1 > num2) num1 else num2
}(4, 8)
Log.d(TAG, "onCreate: ${max}")
利用上下文推断类型
完整的 Lambda 表达式需要定义形参类型,但是如果 kotlin 可以根据 Lambda 表达式上下文推断出形参类型,那么 Lambda 表达式就可以省略形参类型
//由于程序定义了 square 变量的类型,因此可以推断出 Lambda 表达式的形参类型
//所以 Lambda 表达式可以省略形参类型
var square: (Int) -> Int = { n ->
n * n
}
//使用 square 调用 Lambda 表达式
Log.d(TAG, "onCreate: ${square(5)}")
var list = listOf("Kotlin", "Java", "Android")
//使用 Lambda 表达式定义去除条件:长度大于5的集合元素被去除
//由于 doWhile()方法的形参是(T) -> Boolean 类型
//因此调用该方法时可省略形参类型
var resultList = list.dropWhile({ item -> item.length > 5 })
Log.d(TAG, "onCreate: $resultList")
省略形参名
Lambda 表达式不仅可以省略形参类型,而且如果只有一个形参,那么 kotlin 允许省略Lambda 表达式的形参名,如果 Lambda 表达式省略了形参名,那么此时 ->也不需要了, Lambda表达式可通过 it 来代表形参
//省略形参名,用 it 代表形参
var square: (Int) -> Int = {
it * it
}
调用 Lambda 表达式的约定
如果函数的最后一个参数是函数类型,而且你打算传入Lambda 表达式作为相应的参数,那么就允许在圆括号之外指定 Lambda 表达式
var list = listOf("Kotlin", "Java", "Android")
//最后一个参数是 Lambda 表达式,可将表达式写在圆括号外面,输出[Java, Android]
var resultList = list.dropWhile() { item -> item.length > 5 }
Log.d(TAG, "onCreate: $resultList")
var reduce = list.reduce() { acc, e -> acc + e }
//输出KotlinJavaAndroid
Log.d(TAG, "onCreate: reduce=$reduce")
如果 Lambda 表达式是函数调用的唯一参数,则调用方法时的圆括号完全可以省略
var list = listOf("Kotlin", "Java", "Android")
//最后一个参数是 Lambda 表达式,可将表达式写在圆括号外面,输出[Java, Android]
var resultList = list.dropWhile { item -> item.length > 5 }
Log.d(TAG, "onCreate: $resultList")
var reduce = list.reduce { acc, e -> acc + e }
//输出KotlinJavaAndroid
Log.d(TAG, "onCreate: reduce=$reduce")
个数可变的参数和 Lambda 参数
如果一个函数既包含个数可变的形参, 也包含函数类型的形参,那么就应该将函数类型的形参放在最后
fun <T> test(vararg names: String, transform: (String) -> T): List<T> {
var mutableList: MutableList<T> = mutableListOf()
for (name in names) {
mutableList.add(transform(name))
}
return mutableList
}
//调用
test("Java", "Kotlin") { it.length }
匿名函数
Lambda 有一个严重的缺陷:不能指定返回值类型,在一些特殊的场景下,如果 Kotlin 无法推断出 Lambda表达式的返回值类型,此时就需要显式指定返回值类型,而匿名函数即可代替 Lambda 表达式
匿名函数的用法
//定义匿名函数,赋值给 test 变量
var test = fun(x: Int, y: Int): Int {
return x + y
}
//通过 test 调用匿名函数
Log.d(TAG, "onCreate: ${test(5, 7)}")
匿名函数与普通函数基本相似,只要将普通函数的函数名去掉就变成了匿名函数,与普通函数不同的是,如果系统可以推断出匿名函数的形参类型,那么匿名函数允许省略形参类型
var list = listOf("Kotlin", "Java", "Android")
//可以推断出此时list.filter()方法时需要传入一个(String)->Boolean 类型的参数
//因此此处允许省略匿名函数的形参类型
list.filter(
fun(item): Boolean {
return item.length > 4
}
)
匿名函数如果使用普通代码块作为函数体,则匿名函数需要显式指定返回值类型,否则认为该匿名函数没有返回值(相当于 Unit );如果使用单表达式作为函数体,则无须指定返回值类型
var list = listOf("Kotlin", "Java", "Android")
//匿名函数的函数体是单表达式,允许省略声明函数的返回值类型
list.filter(
fun(item) = item.length > 4
)
//定义匿名函数的函数体是单表达式,可以省略声明函数的返回值类型
var sum = fun(x: Int, y: Int) = x + y
Log.d(TAG, "onCreate: ${sum(5, 7)}")
匿名函数和 Lambda 表达式的 return
匿名函数的本质依然是函数,因此匿名函数的 return 则用于返回该函数本身;而 Lambda表达式的 return 用于返回它所在的函数,而不是返回 Lambda 表达式
var list = listOf("Kotlin", "Java", "Android")
//使用匿名函数执行 forEach() 方法,会依次输出Kotlin、Java、Android
list.forEach(
fun(item) {
Log.d(TAG, "onCreate: ${item}")
//匿名函数中的 return 用于返回该函数本身,对 main()函数没有任何影响
return
}
)
//使用 Lambda 表达式执行 forEach()方法,只会输出Kotlin
list.forEach {
Log.d(TAG, "onCreate: ${it}")
//Lambda 表达式中的 return 用于返回它所在的函数(main 函数)
return
}
Lambda 表达式中的 return 用于返回该表达式所在的函数,因此一般不会在 Lambda 表达式中使用 return,如果一定要在 Lambda 表达式中使用return,可使用限定返回的语法
//使用 Lambda 表达式执行 forEach()方法,只会输出Kotlin
list.forEach {
Log.d(TAG, "onCreate: ${it}")
//使用限定返回,此时return只是返回forEach方法的Lambda表达式,因此会完整遍历完list
return@forEach
}
捕获上下文中的变量和常量
Lambda 表达式或匿名函数(以及局部函数、对象表达式)可以访问或修改其所在上下文(俗称“闭包”)中的变量和常量,这个过程被称为捕获。即使定义这些变量和常量的作用域己经不存在了, Lambda 表达式或匿名函数也依然可以访问或修改它们
//定义一个函数,该函数的返回值类型为()->List<String>
fun makeList(ele: String): () -> List<String> {
//创建一个不包含任何元素的 List
var list: MutableList<String> = mutableListOf()
fun addElement(): List<String> {
//向 list 集合中添加一个元素
list.add(ele)
return list
}
return ::addElement
}
上面的 addElement()函数没有定义任何参数,也没有定义任何变量,但该局部函数却访问list变量和 ele 形参,这是因为它捕获了其所在函数中的变量
Lambda 表达式或匿名函数都会持有一个其所捕获的变量的副本,因此表面上看addElement()访问的是 makeList()函数的 list 集合变量,但只要程序返回一个新的addElement()函数, addElement 函数就会自己持有一个新的 list 的副本
var makeList1 = makeList("Kotlin")
//输出[Kotlin]
Log.d(TAG, "onCreate: ${makeList1()}")
//输出[Kotlin, Kotlin]
Log.d(TAG, "onCreate: ${makeList1()}")
var makeList2 = makeList("Java")
//输出[Java]
Log.d(TAG, "onCreate: ${makeList2()}")
//输出[Java, Java]
Log.d(TAG, "onCreate: ${makeList2()}")
内联函数
高阶函数(为函数传入函数或 Lambda 表达式作为参数)的调用过程:
程序要将执行顺序转移到被调用表达式或函数所在的内存地址,当被调用表达式或函数执行完后,再返回到原函数执行的地方
这个转移过程中,系统要处理如下事情:
- 为被调用的表达式或函数创建一个对象
- 为被调用的表达式或函数所捕获的变量创建一个副本
- 在跳转到被调用的表达式或函数所在的地址之前,要先保护现场并记录执行地址:从被调用的表达式或函数地址返回时,要先恢复现场,并按原来保存的地址继续执行。也就是通常所说的压栈和出栈
不难看出,函数调用会产生一定的时间和空间开销,为了避免产生函数调用的过程,我们可以考虑直接把被调用的表达式或函数的代码“嵌入”原来的执行流中,可通过内联函数来实现
内联函数的使用
只要使用 inline 关键字修饰带函数形参的函数即可
//定义函数类型的形参,其中 fn 是(Int) -> Int类型的形参
inline fun map(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
}
//调用
var array = arrayOf(30, 2, 5, 65)
var mapResult = map(array) { it + 3 }
Log.d(TAG, "onCreate: ${mapResult}")
面map()函数包含一个函数类型的形参,且该函数使用了 inline 修饰,因此它是一个内联函数。编译该程序,会发现编译结果只产一个class 文件,不会生成其他额外的内部类的 class 文件,这表明编译器实际上会将 Lambda 表达式的代码复制、粘贴到 map()函数中
内联函数的本质是将被调用的 Lambda 表达式或函数的代码复制、粘贴到原来的执行函数中,因此如果被调用的 Lambda 表达式或函数的代码非常大,势必带来程序代码量的急剧增加
如果被调用的 Lambda 表达式或函数包含大量的执行代码,那么就不应该使用内联函数;如果被调用的 Lambda 表达式或函数只包含非常简单的执行代码(尤其是单表达式),那么就应该使用内联函数
部分禁止内联
使用 inline 修饰函数之后,所有传入该函数的 Lambda 表达式或函数都会被内联化;如果我们又希望该函数中某一个或某几个函数类型的形参不会被内联化,则可使用noinline饰它们,noinline 用于显式阻止某一个或某几个形参内联化
inline fun map(data: Array<Int>, fn: (Int) -> Int, noinline fn2: (String) -> 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
}
那么为什么Kotlin还要提供一个noinline关键字来排除内联功能呢?这是因为内联的函数类型参数在编译的时候会被进行代码替换,因此它没有真正的参数属性。非内联的函数类型参数可以自由地传递给其他任何函数,因为它就是一个真实的参数,而内联的函数类型参数只允许传递给另外一个内联函数。如下:
inline fun testInline(num1: Int, num2: Int, operation: (Int, Int) -> Int): Int {
test(operation)
return operation(num1, num2)
}
fun test(block: (Int, Int) -> Int) {
block(3,6)
}
上面的代码 test(operation) 处会报错,原因如下:
在 inline 函数 testInline 中调用高阶函数 test ,由于 operation 已经被内联化了,没有真正的参数属性,因此无法传递,修改方法有三种,一是 operation 使用 noinline 禁止内联,二是 testInline 函数去掉 inline 关键字,三是 test 也改为 inline 函数,如下是第一种方法:
inline fun testInline(num1: Int, num2: Int, noinline operation: (Int, Int) -> Int): Int {
test(operation)
return operation(num1, num2)
}
fun test(block: (Int, Int) -> Int) {
block(3,6)
}
非局部返回
默认情况下,在 Lambda 表达式中并不允许直接使用return 。这是因为如果是非内联的 Lambda 表达式,该 Lambda 表达式会额外生成一个函数对象,因此这种表达式中的 return 不可能用于返回它所在的函数
由于内联的 Lambda 表达式会被直接复制、粘贴到调用它的函数中,故此时在该 Lambda表达式中可以使用 return ,该 return 就像直接写在 Lambda 表达式的调用函数中一样。因此,该内联的 Lambda 表达式中的 return 可用于返回它所在的函数,这种返回被称为非局部返回
inline fun each(array: Array<Int>, fn: (Int) -> Unit) {
for (item in array) {
fn(item)
}
}
//调用
var array = arrayOf(30, 2, 5, 65)
each(array) {
Log.d(TAG, "onCreate: ${it}")
return //如果 each 函数没有 inline 修饰,此处编译错误
//如果 each 函数有 inline 修饰,此处的 return 将返回 main 函数
}
某些内联函数不是直接从函数体中调用 Lambda 表达式的,而是从其他的执行上下文(如局部对象或局部函数)中来获取 Lambda 表达式的。在这种情况下,非局部返回的控制流也不允许出现在 Lambda 表达式中。此时应该使用 crossinline 来修饰该参数,crossinline 的作用是让被标记的lambda表达式不允许非局部返回
inline fun f(crossinline body: () -> Unit) {
val f = object : Runnable {
override fun run() = body()
//
}
//......
}