函数和Lambda表达式
Kotlin对Java的存粹的面向对象进行了弥补,增加了函数式编程的支持,提高了编程的灵活性。对于Java程序员来讲,Kotlin的函数最需要花精力来掌握的内容。
1. 函数入门
-
定义和调用函数
必须使用
fun
关键字声明。fun main(args: Array<String>) { println("3和5中较大的是${max(3, 5)}") } fun max(a: Int, b: Int): Int { return if (a > b) a else b }
-
函数返回值和
Unit
如果没有返回值可以省略,或者使用
:Unit
fun main(args: Array<String>) { sayHi("JamFF") showMsg("msg", 2) } fun sayHi(name: String) { println("你好,$name") } fun showMsg(msg: String, count: Int): Unit { for (i in 1..count) { println("$i: $msg") } }
-
递归函数
已知数列:f(0) = 1, f(1) = 4, f(n+2) = 2 * f(n+1) + f(n),其中n是大于0的整数,求f(4)的值。
fun main(args: Array<String>) { println("f(4) = ${f(4)}") } fun f(num: Int): Int { return if (num == 0) 1 else if (num == 1) 4 else 2 * f(num - 1) + f(num - 2) }
-
单表达式函数
函数只是返回单个表达式,可以省略花括号并在等号后指定函数体即可。这种方式被称为单表达式函数。
fun main(args: Array<String>) { println(area(2.0, 3.0)) } fun area(x: Double, y: Double): Double = x * y
编译器可以推断出函数返回值类型,还可以省略返回值类型。
fun area(x: Double, y: Double) = x * y
2. 函数的形参
-
命名参数(具名参数)
fun main(args: Array<String>) { println(area(width = 2.0, height = 3.0)) println(area(height = 3.0, width = 2.0)) println(area(2.0, height = 3.0)) // 报错,命名参数必须在位置参数后面 println(area(width = 2.0, 3.0)) // 报错,位置参数是width,后面的命名参数还是width println(area(3.0, width = 2.0)) } fun area(width: Double, height: Double): Double = width * height
-
形参默认值
通过形参默认值,可以减少函数重载数量。
fun main(args: Array<String>) { showMsg() showMsg("Tony", "Hello Java") showMsg("Tony") showMsg(msg = "Hello Java") } fun showMsg(name: String = "JamFF", msg: String = "Hello Kotlin") { println("$name: $msg") }
通常建议将带默认值的参数定义在形参列表的最后。
fun main(args: Array<String>) { printTriangle('*') } // 打印三角形 fun printTriangle(char: Char, height: Int = 5) { for (i in 1..height) { for (j in 0 until height - 1) { print(" ") } for (j in 0 until 2 * i - 1) { print(char) } println() } }
-
尾递归函数
当函数将调用自身作为它执行的最后一行代码,且递归调用后没有更多的代码时可以使用尾递归语法。
另外,尾递归不能在异常处理的try、catch、finall块中使用。
尾递归函数需要使用tailrec
修饰。fun main(args: Array<String>) { println(fact(4))// 输出24 println(facRec(4))// 输出24 } // 普通递归,计算阶乘。这里的返回值不能省略,使用递归else返回值不明确 fun fact(n: Int): Int = if (n == 1) 1 else n * fact(n - 1) // 尾递归优化,计算阶乘。这里的返回值不能省略,使用递归else返回值不明确 tailrec fun facRec(n: Int, total: Int = 1): Int = if (n == 1) total else facRec(n - 1, total * n)
与普通递归相比,编译器会进行优化,减少内存消耗。
-
个数可变的形参(可变参数)
在形参的类型前添加
vararg
修饰,表示该形参可以接受多个参数值,多个参数值被当作数组传入。fun main(args: Array<String>) { test(5, "JamFF", "Jason") } fun test(a: Int, vararg names: String) { for (name in names) { println(name) } println(a) }
可变参数可以处于参数列表的任意位置(不要求是最后一个参数),但一个函数最多只能带一个可变参数,并且如果给可变参数后面的参数传参,必须使用命名参数。
fun main(args: Array<String>) { test("JamFF", "Jason", num = 5) } fun test(vararg names: String, num: Int) { for (name in names) { println(name) } println(num) }
如果已经有一个数组,希望把数组传入可变参数,可以在数组参数前添加
*
运算符。fun main(args: Array<String>) { val arr = arrayOf("JamFF", "Jason") test(*arr, num = 5) }
3. 函数重载
与Java一致。
如果被重载的函数包含可变参数,Kotlin会尽量执行最精确的匹配。
fun main(args: Array<String>) {
test()// 可变参数
test("JamFF", "Jason")// 可变参数
test("JamFF")// 一个参数
}
fun test(msg: String) {
println("只含有一个字符串的test函数 $msg")
}
fun test(vararg names: String) {
println("可变参数 ${names.contentToString()}")
}
大部分情况下不推荐重载可变参数的函数,没有意义并且容易导致错误。
4. 局部函数
Kotlin支持在函数体内部定义函数,这种函数被称为局部函数。
默认情况下,局部函数对外部是隐藏的,局部函数只能在其封闭(enclosing)函数内有效,其封闭函数也可以返回局部函数,以便程序在其他作用域中使用局部函数。
fun main(args: Array<String>) {
println(getMathFunc("square", 3))
println(getMathFunc("cube", 3))
println(getMathFunc("factorial", 3))
}
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
}
// 使用when表达式,简化多个return
return when (type) {
"square" -> square(nn)
"cube" -> cube(nn)
else -> factorial(nn)
}
}
如果程序使用变量保存了封闭函数的返回值,那么这个局部函数的作用域就会被扩大,和全局函数是一样的。
5. 高阶函数
Kotlin的函数是一等公民,也就是说可以想变量一样使用。高阶函数就是,以另一个函数作为参数或返回值的函数。
-
使用函数类型
定义函数类型的变量
fun main(args: Array<String>) { // 定义一个变量,类型为(Int, Int) -> Int val myFun: (Int, Int) -> Int // 定义一个变量,类型为(String) -> Unit,返回值不能省略 val test: (String) -> Unit }
赋值使用
fun main(args: Array<String>) { // 定义一个变量,类型为(Int, Int) -> Int var myFun: (Int, Int) -> Int // 定义一个变量,类型为(String) -> Unit,返回值不能省略 val test: (String) -> Unit test = ::show// 在函数前面使用::表示函数的引用 test("JamFF") myFun = ::pow println(myFun(2, 4))// 计算2的4次方,输出16 myFun = ::area println(myFun(2, 4))// 计算面积,输出8 } fun show(msg: String) { println(msg) } // 计算乘方 fun pow(base: Int, exponent: Int): Int { var result = 1 for (i in 1..exponent) { result *= base } return result } // 计算面积 fun area(width: Int, height: Int): Int { return width * height }
-
使用函数类型作为形参类型
fun main(args: Array<String>) { val data = arrayOf(1, 2, 3, 4, 5) println("原数据${data.contentToString()}") println("计算元素平方${map(data, ::square).contentToString()}") println("计算元素立方${map(data, ::cube).contentToString()}") println("计算元素阶乘${map(data, ::factorial).contentToString()}") } // 平方 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 } // fn是函数类型的形参 fun map(data: Array<Int>, fn: (Int) -> Int): Array<Int> { val result = Array(data.size) { 0 } for (i in data.indices) { result[i] = fn(data[i]) } return result }
-
使用函数类型作为返回值类型
fun main(args: Array<String>) { var mathFunc = getMathFunc("cube") println(mathFunc(5))// 输出125 mathFunc = getMathFunc("square") println(mathFunc(5))// 输出25 mathFunc = getMathFunc("other") println(mathFunc(5))// 输出120 } // 返回值类型是函数类型 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 } }
6. 局部函数与Lambda表达式
如果说函数是命名的、方便复用的代码块,那么Lambda表达式则是功能更灵活的代码块,它可以在程序中被传递和调用。
-
使用Lambda表达式代替局部函数
使用Lambda表达式简化上面的代码。
fun main(args: Array<String>) { var mathFunc = getMathFunc("cube") println(mathFunc(5))// 输出125 mathFunc = getMathFunc("square") println(mathFunc(5))// 输出25 mathFunc = getMathFunc("other") println(mathFunc(5))// 输出120 } // 返回值类型是函数类型 fun getMathFunc(type: String): (Int) -> Int { // 返回局部函数 return when (type) { "square" -> { n: Int -> n * n } "cube" -> { n: Int -> n * n * n } else -> { n: Int -> var result = 1 for (index in 2..n) { result *= index } result } } }
-
Lambda表达式的脱离
作为函数参数传入的Lambda表达式可以脱离函数独立使用。
fun main(args: Array<String>) { collectFn { a: Int -> a * a } // 如果只有一个形参,Kotlin可以省略形参名,如果省略的话,->也不需要了,用it代替形参。 collectFn { it * it }// 上面表达式的简写,在下面Lambda表达式会详细说到 collectFn { it * it * it } for (i in lambdaList.indices) { println(lambdaList[i](i + 2)) } } // 定义一个List类型的变量 val lambdaList = ArrayList<(Int) -> Int>() // 返回值类型是函数类型 fun collectFn(fn: (Int) -> Int) { // 将传入的fn(函数或Lambda表达式)添加到集合 lambdaList.add(fn) }
上面程序把Lambda表达式作为参数传给collectFn()
函数后,这些Lambda表达式可以脱离collectFn()
函数使用。
7. Lambda表达式
Lambda表达式的标准语法如下:
{ (形参列表) ->
// 零条到多条可执行语句
}
- Lambda表达式总是被大括号括着。
- 定义Lambda表达式不需要
fun
关键字,无须制定函数名。 - 形参列表(如果有的话)在
->
之前声明,参数类型可以省略。 - 函数体(Lambda表达式执行体)放在
->
之后。 - 函数的最后一个表达式自动被作为Lambda表达式的返回值,无须使用
return
关键字。
-
调用Lambda表达式
可以将Lambda表达式赋值给变量或直接调用Lambda表达式
fun main(args: Array<String>) { // 将Lambda表达式赋值给变量 val square = { n: Int -> n * n } println(square(5))// 输出25 // 在Lambda表达式后面添加圆括号,直接调用 val result = { base: Int, exponent: Int -> var result = 1 for (i in 1..exponent) { result *= base } result }(4, 3) println(result)// 输出64 }
-
利用上下文推断类型
完整的Lambda表达式需要定义形参类型,但是如果Kotlin可以根据Lambda表达式上下文推断出形参类型,那么就可以省略形参类型。
fun main(args: Array<String>) { // Lambda表达式被赋值为(Int) -> Int类型的变量,可以推断形参类型 val square: (Int) -> Int = { n -> n * n } println(square(5))// 输出25 // 直接传参(4, 3),不能推断形参类型,所以需要显式声明形参类型 val result = { base: Int, exponent: Int -> var result = 1 for (i in 1..exponent) { result *= base } result }(4, 3) println(result)// 输出64 val list = listOf("Java", "Kotlin", "Go") // 因为dropWhile方法的形参是(T) -> Boolean类型,所以可以推断形参类就是集合元素类型 // dropWhile返回一个新List,返回从第一项起,去掉满足条件的元素(lambda返回true),直到不满足条件的一项为止 val rt = list.dropWhile({ e -> e.length > 3 }) // Lambda可以简化,在下面Lambda表达式会详细说 // val rt = list.dropWhile { it.length > 3 } println(rt)// 输出[Go] }
-
省略形参名
Lambda表达式不仅可以省略形参类型,而且如果只有一个形参,那么Kotlin允许省略Lambda表达式的形参名,如果省略了,那么->也不需要了,Lambda表达式可通过
it
来代表形参。fun main(args: Array<String>) { // 用it代表形参 val square: (Int) -> Int = { it * it } println(square(5))// 输出25 val list = listOf("Java", "Kotlin", "Go") // 用it代表形参 val rt = list.dropWhile({ it.length > 3 }) println(rt)// 输出[Go] }
-
调用Lambda表达式的约定
如果函数的最后一个参数是函数类型,并且你打算传入一个Lambda表达式作为相应的参数,那么就允许在圆括号之外指定Lambda表达式。
fun main(args: Array<String>) { val list = listOf("Java", "Kotlin", "Go") // 最后一个参数是Lambda表达式,可以将表达式写在圆括号外面 val rt = list.dropWhile() { it.length > 3 } println(rt)// 输出[Go] val map = mutableMapOf("Android" to 666) // 最后一个参数是Lambda表达式,可以将表达式写在圆括号外面 list.associateTo(map) { it to it.length } println(map)// 输出{Android=666, Java=4, Kotlin=6, Go=2} // 最后一个参数是Lambda表达式,可以将表达式写在圆括号外面 val rtx = list.reduce() { acc, s -> acc + s } println(rtx)// 输出JavaKotlinGo }
如果Lambda表达式是函数调用的唯一参数,调用方法时的圆括号可以省略。
val rt = list.dropWhile { it.length > 3 } val rtx = list.reduce { acc, s -> acc + s }
通常建议将函数类型的形参放在参数列表的最后,方便传入Lambda表达式作为参数。
-
个数可变的参数和Lambda参数
虽然Kotlin允许将可变参数定义在形参列表的任意位置,但如果不将可变参数定义在最后,那么就只能用命名参数的形式给可变参数之后的其他形参传值。
但上面又建议将函数类型的参数放在形参列表的最后。如果一个函数既包含个数可变的形参,也包含函数类型的形参,这就产生了冲突。Kotlin约定:如果调用函数时最后一个参数是Lambda表达式,则可将Lambda表达式放在圆括号外面,这样就无需使用命名参数了。
因此答案是:将函数类型的形参放在最后。
fun <T> test(vararg names: String, transform: (String) -> T): List<T> { val mutableList: MutableList<T> = mutableListOf() for (name in names) { mutableList.add(transform(name)) } return mutableList } fun main(args: Array<String>) { val list1 = test("Java", "Kotlin", "Go") { it.length } println(list1)// 输出[4, 6, 2] val list2 = test("Java", "Kotlin", "Go") { "$it${it.length}个字" } println(list2)// [Java4个字, Kotlin6个字, Go2个字] }
8. 匿名函数
Lambda表达式虽然简介、方便但是不能指定返回值类型。大部分时候,Kotlin可以推断出Lambda表达式的返回值类型。但在一些特殊的场景下无法推断,就需要显式指定返回值类型,而匿名函数即可代替Lambda表达式。
-
匿名函数的用法
匿名函数与普通函数类似,只要将普通函数去掉函数名就成了匿名函数。
fun main(args: Array<String>) { // Lambda表达式定义变量 val test1 = { x: Int, y: Int -> x + y } // 匿名函数定义变量 val test2 = fun(x: Int, y: Int): Int { return x + y } println(test1(2, 4))// 输出6 println(test2(2, 4))// 输出6 }
与普通函数不同的是,如果可以推断出匿名函数的形参类型,那么匿名函数允许省略形参类型。
fun main(args: Array<String>) { val filteredList = listOf(3, 5, 20, 100, -25).filter( // filter()方法需要传入一个(Int) -> Boolean类型的参数 // 当传入匿名函数时,可以推断出参数类型必须是(Int) -> Boolean,可以省略形参类型 fun(el): Boolean { return Math.abs(el) > 20 } ) println(filteredList)// 输出[100, -25] // filter()传入Lambda表达式 val filteredList2 = listOf(3, 5, 20, 100, -25).filter { Math.abs(it) > 20 } println(filteredList2)// 输出[100, -25] }
匿名函数的返回值类型的声明规则与普通函数相同。如果使用单表达式作为函数体,则无须指定返回值类型,系统可自动推断。
fun main(args: Array<String>) { // 单表达式作为函数体,省略返回值类型 val test = fun(x: Int, y: Int) = x + y val filteredList = listOf(3, 5, 20, 100, -25).filter( // 单表达式作为函数体,省略返回值类型 fun(el) = Math.abs(el) > 20 ) println(test(2, 4))// 输出6 println(filteredList)// 输出[100, -25] }
-
匿名函数和Lambda表达式的return
匿名函数的本质依然是函数,因此匿名函数中的
return
用于返回函数本身;而lambda表达式的return
用于返回它所在的函数。fun main(args: Array<String>) { val list = listOf(3, 5, 30, -25, 14) list.forEach(fun(n) { println(n)// 全部输出 return }) list.forEach { println(it)// 只输出3 return } }
如果一定要在Lambda中使用return,返回该函数本身,可以使用限定返回的语法。
list.forEach { println(it)// 只输出3 // 使用限定返回,此时return只是返回给forEach方法的Lambda表达式 return@forEach }
9. 捕获上下文中的变量和常量
Lambda表达式或匿名函数(以及局部函数、对象表达式)可以访问或修改其所在上下文(俗称“闭包”)中的变量和常量,这个过程被称为捕获。即使定义这些变量和常量的作用域已经不存在,Lambda表达式或匿名函数也依然可以访问或修改它们。
例如下面先定义一个函数,然后在该函数内定义局部函数,此时局部函数就可以访问或修改其所在上下文(函数)中的变量。
// 定义一个函数,返回值类型是 () -> List<String>
fun makeList(ele: String): () -> List<String> {
// 创建一个不包含任何元素的List
val list: MutableList<String> = mutableListOf()
// 局部函数,没有定义任何变量,却可以访问list和ele,因为捕获了其所在函数的变量
fun addElement(): List<String> {
list.add(ele)
return list
}
return ::addElement
}
fun main(args: Array<String>) {
println("-----add1 返回的List-----")
val add1 = makeList("Java")
println(add1())
println(add1())
println("-----add2 返回的List-----")
val add2 = makeList("Kotlin")
println(add2())
println(add2())
}
运行结果
-----add1 返回的List-----
[Java]
[Java, Java]
-----add2 返回的List-----
[Kotlin]
[Kotlin, Kotlin]
10. 内联函数
先简单介绍一下高阶函数(为函数传入函数或Lambda表达式作为函数)的调用过程。调用Lambda表达式或函数的过程是:程序要将执行顺序转移到被调用表达式或函数所在的内存地址,当被调用表达式或函数执行完后,再返回到原函数执行的地方。
在上面这个转移过程中,系统要处理如下事情。
- 为被调用的表达式或函数创建一个对象。
- 为被调用的表达式或函数所捕捉的变量创建一个副本。
- 在跳转到被调用的表达式或函数所在的地址之前,要先保护现场并记录执行地址;从被调用的表达式或函数地址返回时,要先恢复现场,并按原来保存的地址继续执行。也就是通常说的压栈和弹栈。
不难看出,函数调用会产生一定的时间和空间开销,如果被调用的表达式或函数的代码量本身不大,而且经常被调用,那么这个时间和空间开销的损耗就很不划算。
为了避免产生函数调用的过程,可以考虑直接把被调用的表达式或函数的代码“嵌入”原来的执行流中——简单来说,就是编译器负责“复制、粘贴”:复制被调用的表达式或函数的代码,然后粘贴到原来的执行代码中。为了让编译器帮我们干这个复制、粘贴的话,可通过内联函数来实现。
-
内联函数的使用
只要使用
inline
关键字修饰带函数形参的函数即可。下面示范来内联函数和非内联函数的区别。inline fun map(data: Array<Int>, fn: (Int) -> Int): Array<Int> { val result = Array(data.size) { 0 } for (i in data.indices) { result[i] = fn(data[i]) } return result } fun main(args: Array<String>) { val arr = arrayOf(20, 4, 40, 100, 30) val mappedResult = map(arr) { it + 3 } println(mappedResult.contentToString()) }
使用
inline
内联函数,编译器实际上会将Lambda表达式的代码复制、粘贴到map()
函数中。也就是说,程序调用的map
函数编译后实际上变成了如下形式:fun map(data: Array<Int>): Array<Int> { val result = Array(data.size) { 0 } for (i in data.indices) { result[i] = data[i] + 3 } return result }
需要注意的是,内联函数并不总是能带来好处,因为内联函数的本质是将被调用的Lambada表达式或函数的代码复制、粘贴到原执行函数中。当Lambada表达式或函数包含大量的执行代码,不应使用内联函数;如果Lambada表达式或函数只包含非常简单的执行代码(尤其是单表达式),那么就应该使用内联函数。
-
部分禁止内联
使用
inline
修饰后,所有传入函数的Lambda表达式或函数都会被内联化,如果希望该函数中的某个或某几个函数类型的形参不被内联化,可以使用noinline
修饰。inline fun test(fn1: (Int) -> Int, noinline fn2: (String) -> String) { println(fn1(8)) println(fn2("Kotlin")) } fun main(args: Array<String>) { test({ it * it }, { it }) }
上面程序虽然使用
inline
修饰了test()
函数,但是fn2
形参使用了noinline
修饰,它不会被内联化。 -
非局部返回
前面提到在Lambda表达式中使用
return
不是用于返回该表达式,而是返回该表达式所在的函数。但要记住:默认情况下,在Lambda表达式中并不允许直接使用return
。这是因为如果是非内联的Lambda表达式会额外生成一个函数对象,因此这种表达式中的return
不可能用于返回它所在的函数。由于内联的Lambda表达式会被直接复制、粘贴到调用它的函数中,故此在该Lambda表达式中可使用
return
,该return
就像直接写在Lambda表达式的调用函数中一样。因此,该内联的Lambda表达式中的return
可用于返回它所在的函数,这种返回被称作非局部返回。inline fun each(data: Array<Int>, fn: (Int) -> Unit) { for (el in data) { fn(el) } } fun main(args: Array<String>) { val arr = arrayOf(20, 4, 40, 100, 30) each(arr) { if (it == 4) { return@each// 返回each函数 } if (it == 100) { //return// 如果each没有inline修饰,此处编译异常 return // 如果each有inline修饰,return返回main函数 } println(it) } }
如果删除上面
each()
函数的inline
修饰符,那么下面的Lambda表达式中的return
将会提示编译错误:'return' is not allowed here
,这意味着在非内联的Lambda表达式中不能使用return
。另外,有些内联函数不是从函数体中调用Lambda表达式的,而是从其他的执行上下文(如局部对象或局部函数)中来获取Lambda表达式的。在这种情况下,非局部返回的控制流也不与许出现在Lambda表达式中。此时应该使用
crossinline
来修饰该参数。inline fun f(crossinline body: () -> Unit) { val f1 = object : Runnable { override fun run() { body() } } // Lambda简写,和上面f1一样 val f2 = Runnable { body() } }
重点
- 函数、调用函数的语法
- 函数形参的外部形参名、形参默认值、常量形参和可变参数、In-Out形参等高级特性
- 函数类型可以被当作数组类型使用,即可用于声明变量,也可作为形参或者返回值
- Lambda表达式