高阶函数
Kotlin不是纯粹的面向对象语言,Kotlin的函数也是一等公民,因此函数本身也具有自己的类型。 函数类型就像前面介绍的数据类型一样,既可用于定义变量 ,也可用作函数的形参类型,还可作为函数的返回值类型。
使用函数类型
Kotlin的每个函数都有特定的类型,函数类型由函数的形参列表、 ->和返回值类型组成。 例如如下函数:
fun foo(a: Int, name: String): String {
//...
}
该函数的形参列表、->和返回值类型为(Int, String) -> String,这就是该函数的类型。
fun foo(a: Double, b: Double){
//...
}
该函数的形参列表、->和返回值类型为(Double, Double)-> Unit或(Double,Double),这就是该函数的类型。
fun fooc(){
//...
}
该函数的形参列表、->和返回值类型为() ->Unit或(),这就是该函数的类型。
掌握了函数类型之后,接下来就可以使用函数类型定义变量了,就像使用普通类型定义变量一样.
fun main(args: Array<String>) {
//定义一个变量,其类型为(Int , Int) -> Int
var myFun: (Int, Int) -> Int
//定义一个变量,其类型为(String)
var test: (String)
//将 area1 函数赋值给 myfun,则 myfun 可当成 area1 使用
myFun = ::area1
println(myFun(9,10))
//将 add1 函数赋值给 myfun,则 myfun 可当成 add1 使用
myFun = ::add1
println(myFun(8,2))
}
fun area1(x: Int, y: Int): Int = x * y
fun add1(x:Int,y:Int):Int{
return x+y;
}
从上面代码可以看出,程序依次将 area1()、 add1()函数赋值给 myFun 变量,只要被赋值的函数类型与 myfun 的变量类型一致,程序就可以赋值成功。
当直接访问一个函数的函数引用,而不是调用函数时,需要在函数名前添加两个冒号,而且不能在函数后面添加圆括号,一旦添加圆括号,就变成了调用函数,而不是访问函数引用。通过使用函数类型的变量,可以让 myFun在不同的时间指向不同的函数,从而让程序更加灵活 。 由此可见,使用函数类型的好处是让程序更加灵活。 除此之外,程序还可使用函数类型作为形参类型和返回值类型。
使用函数类型作为形参类型
有时候需要定义一个函数, 该函数的大部分计算逻辑都能确定,但某些处理逻辑暂时无法确定,这意味着某些程序代码需要动态改变,如果希望调用函数时能动态传入这些代码,就需要在函数中定义函数类型的形参,这样即可在调用该函数时传入不同的函数作为参数,从而动态改变这些代码 。
fun main(args: Array<String>) {
var data = arrayOf(3,4,9,5,8)
println("原数据${Arrays.toString(data)}")
//下面程序代码调用 map ()函数 3 次 , 每次调用时都传入不同的函数
println ("计算数组元素的平方")
println(map(data, ::square).contentToString())
println ("计算数组元素的立方")
println(map(data, ::cube).contentToString())
println ("计算数组元素的阶乘")
println(map(data, ::factorial).contentToString())
}
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
}
//定义一个计算平方的函数
fun square(num: Int): Int {
return num * num
}
//定义一个计算立方的函数
fun cube(num: Int): Int {
return num * num * num
}
//定义一个计算阶乘的函数
fun factorial(num: Int): Int {
var temp:Int=1
for (i in num downTo 1){
temp=temp*i
}
return temp
}
从上面介绍不难看出,定义了函数类型的形参后,就可以在调用函数时动态地传入函数,实际上可以动态地改变被调用函数的部分代码 。
使用函数类型作为返回值类型
前面已经提到, Kotlin还支持定义函数类型的返回值,这样即可将其他函数作为函数的返回值。 例如如下程序。
fun main(args: Array<String>) {
//调用 getMathFunc (),程序返回一个(Int)→Int 类型的函数
var mathFunc = getMathFunc("cube") //得到 cube 函数
println(mathFunc(5))
}
//定义函数,该函数的返回值类型是(Int)-> Int
fun getMathFunc(type: String): (Int) -> Int {
//定义一个计算平方的函数
fun square(num: Int): Int {
return num * num
}
//定义一个计算立方的函数
fun cube(num: Int): Int {
return num * num * num
}
//定义一个计算阶乘的函数
fun factorial(num: Int): Int {
var temp: Int = 1
for (i in num downTo 1) {
temp = temp * i
}
return temp
}
when (type) {
//返回局部函数
"square" -> return ::square
"cube" -> return ::cube
else -> return ::factorial
}
}
一旦定义了返回值类型为(Int)-> Int 的 getMathFunc()函数,接下来程序调用 getMathFunc() 函数时就可以返回(Int) -> Int 类型的函数。 上面程序中通过调用 getMathFunc()函数,分别得到(Int)->Int函数。
局部函数与 Lambda 表达式
如果说函数是命名的、方便复用的代码块,那么 Lambda表达式则是功能更灵活的代码块,它可以在程序中被传递和调用 。
回顾局部函数
fun main(args: Array<String>) {
//调用 getMathFunc (),程序返回一个(Int)→Int 类型的函数
var mathFunc = getMathFunc("cube") //得到 cube 函数
println(mathFunc(5))
}
//定义函数,该函数的返回值类型是(Int)-> Int
fun getMathFunc(type: String): (Int) -> Int {
//定义一个计算平方的函数
fun square(num: Int): Int {
return num * num
}
//定义一个计算立方的函数
fun cube(num: Int): Int {
return num * num * num
}
//定义一个计算阶乘的函数
fun factorial(num: Int): Int {
var temp: Int = 1
for (i in num downTo 1) {
temp = temp * i
}
return temp
}
when (type) {
//返回局部函数
"square" -> return ::square
"cube" -> return ::cube
else -> return ::factorial
}
}
由于局部函数的作用域默认仅停留在其封闭函数之内,因此这 3 个局部函数的函数名的作用太有限了,仅仅就是在程序的 when表达式中作为返回值使用。 一旦离开了getMathFunc() 函数体,这3个局部函数的函数名就失去了意义。
既然局部函数的函数名没有太大的意义,那么就考虑使用 Lambda表达式来简化局部函数的写法 。
使用 Lambda 表达式代替局部函数
如果使用 Lambda表达式来简化上面的代码,则可以将程序改写成如下形式:
fun main(args: Array<String>) {
//调用 getMathFunc (),程序返回一个(Int)→Int 类型的函数
var mathFunc = getMathFunc("ss") //得到 cube 函数
println(mathFunc(5))
}
//定义函数,该函数的返回值类型是(Int)-> Int
fun getMathFunc(type: String): (Int) -> Int {
//该函数返回 的是 Lambda 表达式
when (type) {
"square" -> return { n ->
n * n
}
"cube" -> return { n: Int ->
n * n * n
}
else -> return { n: Int ->
var result = 1
for (i in n downTo 1) {
result = result * i
}
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)
}
fun main(args: Array<String>) {
//调用 collectFn ()函数两次,将会向 lambdaList 中添加元素(每个元素都是 Lambda 表达式)
collectFn({ it * it })
collectFn({ it * it * it })
//输出 lambdaList 的长度
println(lambdaList.size)
//依次调用 lambdaList 集合的元素(每个元素都是 Lambda 表达式)
//输出100 1331
for( i in lambdaList. indices ) {
println( lambdaList[i](i + 10))
}
}
上面定义了collectFn()函数,该函数带了一个函数类型的形参,在 collectFn()函数内部只是将传入的fn参数添加到 lambdaList集合中,这意味着程序接下来可通过 lambdaList集合访问传给 collectFn()函数的 Lambda 表达式。
从上面程序可以看出,把 Lambda表达式作为参数传给 collectFn()函数之后,这些 Lambda 表达式可以脱离 collectFn()函数使用。
Lambda 表达式
Lambda 表达式的标准语法如下 :
{(形参列表)->
//零到多条执行语句
}
调用 Lambda 表达式
Lambda 表达式的本质是功能更灵活的代码块,因此完全可以将 Lambda 表达式赋值给变量或直接调用 Lambda 表达式
fun main(args: Array<String>) {
//定义一个 Lambda 表达式 ,并将它赋值给 square 变量
var square = { n: Int ->
n * n
}
//使用 square 调用 Lambda 表达式
println(square(5)) //输出 25
//定义一个 Lambda 表达式 ,并在它后面添加()来调用该Lambda 表达式
var result = { base: Int, exponent: Int ->
var result = 1
for (i in 1..exponent){
result *= base
}
result
}(4,3)
println(result)
}
上面程序中定义了两个 Lambda表达式,其中第一个 Lambda表达式被赋值给 square变量,因此程序以后就可以通过该变量重复调用该 Lambda表达式。
程序在第二个 Lambda表达式的后面使用圆括号执行调用,并传入了对应的参数,这样 result变量得到的不是 Lambda表达式,而是执行它的返回值。
利用上下文推断类型
完整的 Lambda 表达式需要定义形参类型 ,但是如果 Kotlin 可以根据 Lambda 表达式上下文推断出形参类型,那么 Lambda表达式就可以省略形参类型。
fun main(args: Array<String>) {
//由于程序定义了 square 变量的类型
//因此 Kotlin 可以推断出 Lambda 表达式的形参类型
//所以 Lambda 表达式可以省略形参类型
var square: (Int) -> Int = { n -> n * n }
//使用 square 调用 Lambda 表达式
println(square(5))
//此时 Kotlin 无法推断出 base、 exponent 两个形参的类型
//因此必须为其指定类型
var result = { base: Int, exponent: Int ->
var result = 1
for (i in 1..exponent){
result *= base
}
result
}(4,3)
println(result) //输出 64
var list = listOf<String>("sdsd","ccc","qqq")
//使用 Lambda 表达式定义去除条件 : 长度大于 3 的集合元素被去除
//由于 doWhile ()方法的形参是(T) -> Boolean 类型
//因此调用该方法时可省略形参类型
var rt = list.dropWhile { e ->e.length>3 }
println(rt)
}
上面程序中第一个 Lambda表达式被赋值给(Int)-> Int类型的变量,这样 Kotlin可以很容易地推断出该 Lambda 表达式的形参类型,因此 Lambda 表达式可以省略形参类型 。 第二个 Lambda 表达式后面紧跟着(4,3), Kotlin 无法准确推断出该 Lambda 表达式的形参类型,因此需要显式声明 Lambda 表达式的形参类型 ; 上面代码调用了 list 的 dropWhile()方法,该方法的形参是(T) -> Boolean 类型 , 因此 Kotlin可以推断出该 Lambda 表达式的形参类型就是集合元素的类型 。
省略形参名
Lambda 表达式不仅可以省略形参类型,而且如果只有一个形参,那么 Kotlin 允许省略 Lambda表达式的形参名。如果 Lambda表达式省略了形参名,那么此时->也不需要了,Lambda 表达式可通过 it来代表形参 。
fun main(args: Array<String>) {
//省略形参名,用 it 代表形参
var square: (Int) -> Int = { it * it }
//使用 square 调用 Lambda 表达式
println(square(5))
//Lambda 表达式有两个形参 , 无法省略
var result = { base: Int, exponent: Int ->
var result = 1
for (i in 1..exponent) {
result *= base
}
result
}(4, 3)
println(result) //输出 64
var list = listOf<String>("sdsd", "ccc", "qqq")
//省略形参名,用 it代表形参
var rt = list.dropWhile { it.length > 3 }
println(rt)
}
调用 Lambda 表达式的约定
Kotlin 语言有一个约定:如果函数的最后一个参数是函数类型,而且你打算传入一个Lambda表达式作为相应的参数,那么就允许在圆括号之外指定 Lambda 表达式 。如果 Lambda表达式是函数调用的唯一参数,则调用方法时的圆括号完全可以省略。
其实这种用法也不是Kotlin独有的,在其他语言中这种用法被称为“尾随闭包(Tail Closure)”。
fun main(args: Array<String>) {
var list = listOf<String>("sdsd", "ccc", "qqq")
//最后一个参数是 Lambda 表达式,可将表达式写在圆括号外面
var rt = list.dropWhile() { it.length > 3 }
println(rt)
var map = mutableMapOf("java" to 56)
//最后一个参数是 Lambda 表达式,可将表达式写在圆括号外面
list.associateTo(map) { "我的${it}" to it.length }
//{java=56, 我的sdsd=4, 我的ccc=3, 我的qqq=3}
println(map)
//最后一个参数是 Lambda 表达式,可将表达式写在困括号外面
var rtx = list.reduce(){acc, e->acc+e}
}
代码分别调用了 List集合的 dropWhile()、 associateTo()、 reduce()方法,其最后一个参数都是 Lambda 表达式,因此程序在调用它们时可将 Lambda 表达式写在外面 。
所以通常我们建议将函数类型的形参放在形参列表的最后,这样方便以后传入 Lambda 表达式作为参数。
个数可变的参数和 Lambda 参数
前面介绍函数的个数可变的形参时提到:虽然 Kotlin允许将个数可变的形参定义在形参列表的任意位置,但如果不将个数可变的形参放在形参列表的最后,那么就只能用命名参数的形式为可变形参之后的其他形参传入参数值 。
但刚才又建议将函数类型的形参放在形参列表的最后,此时就产生了一个问题 : 到底是将个数可变的形参放在最后,还是将函数类型的形参放在最后呢?
Kotlin 约定:如果调用函数时最后一个参数是 Lambda 表达式,则可将 Lambda 表达式放在圆括号外面,这样就无须使用命名参数了。因此答案是 :如果一个函数既包含个数可变的形参,也包含函数类型的形参,那么就应该将函数类型的形参放在最后 。
fun <T> test1(vararg names: String, transform: (String) -> T): List<T> {
var mutableList :MutableList<T> = mutableListOf()
for (name in names){
mutableList.add(transform(name))
}
return mutableList.toList()
}
fun main(args: Array<String>) {
//将 Lambda 表达式放在圆括号后面,无须使用命名参数
var list1 = test1("Java","Kotlin","C#"){it.length}
println(list1)
}
上面程序中第一行代码定义了一个函数,该函数声明了两个形参,第一个是个数可变的形参(可传入多个参数值),第二个是函数类型的形参(可传入 Lambda表达式)。
上面程序中的后面代码调用 test()方法时第二个参数是 Lambda 表达式,因此该参数可放在圆括号外面,而且无须使用命名参数。
网友评论