函数和Lambda表达式(二)

高阶函数

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 表达式,因此该参数可放在圆括号外面,而且无须使用命名参数。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容