Kotlin修炼指南(二):lambda表达式的精髓

2019-11-10 16:15:41 浏览数 (1)

lambda表达式是Kotlin函数式编程的一个重要概念,要想掌握函数式编程,就必须熟练掌握lambda表达式,并掌握它的各种写法和实现,这些都是掌握函数式编程的基础。

lambda基本形式

lambda表达式有三大特征:

  1. lambda表达式存在于{}中
  2. 参数及参数类型(可省略)在->左边
  3. 函数体在->右边

lambda表达式返回值总是返回函数体内部最后一行表达式的值

这三种形式的lambda表达式必须要能够非常熟练的掌握,这样才能进一步的了解Kotlin和函数式编程。

无参数

无参数形式为:

val 函数名 = { 函数体 }

示例:

代码语言:javascript复制
val hello = { println("hello kotlin") }

// 等价于函数
fun hello() {
    println("hello kotlin")
}

有参数

  1. 完整表达方式:

val 函数名 : (参数1类型, 参数2类型, ...) -> 返回值类型 = { 参数1, 参数2, ... -> 函数体 }

  1. 表达式返回值类型可自动推断形式

val 函数名 = { 参数1:类型1, 参数2:类型2, ... -> 函数体 }

示例:

代码语言:javascript复制
val sum: (Int, Int) -> Int = { a, b -> a   b }
// 等价于
val sum = { a: Int, b: Int -> a   b }

// 等价于函数
fun sum(a: Int, b: Int): Int {
    return a   b
}

只有一个参数的时候,返回值中的参数形参可以省略,引用时通过it进行引用

lambda的调用有两种方式,一种是通过()来进行调用,另一种是通过invoke()函数进行调用,两种方式没有区别。

代码语言:javascript复制
fun main(args: Array<String>) {
    val lambda = { println("test") }
    lambda()
    lambda.invoke()
}

在使用lambda表达式的时候,可以用下划线(_)表示未使用的参数,表示不处理这个参数。

匿名函数

匿名函数形式为:

val 函数名 = fun(参数1:类型1, 参数2:类型2, ...): 返回值类型 { 函数体 }

示例:

代码语言:javascript复制
val sum = fun(a: Int, b: Int): Int {
    return a   b
}

// 等价于函数
fun sum(a: Int, b: Int): Int {
    return a   b
}

高阶函数的演变

所谓高阶函数,实际上就是数学中的复合函数的概念,f(g(x))。

引用函数

代码语言:javascript复制
fun cal(a: Int, b: Int, f: (c: Int, d: Int) -> Int): Int {
    return f(a, b)
}

fun sum(a: Int, b: Int): Int {
    return a   b
}

fun main(args: Array<String>) {
    val result = cal(2, 3, ::sum)
    println("result = $result")
    // result = 8
}

在cal函数中的最后一个参数是 f: (a: Int, b: Int) -> Int 表示该参数是一个函数引用,函数体内调用了最后一个参数指向的函数。随后定义了sum函数,该函数就是cal函数的第三个参数。

::sum表示sum函数的引用,cal(2, 3, ::sum)这一句就相当于执行了sum(2, 3),所以输出结果为5。

函数引用可以进一步的简化函数的调用,类似下面这个例子:

代码语言:javascript复制
class Test {
    fun doSomething() {
        println("test")
    }

    fun doTest(f: (Test) -> Unit) {
        f(this)
    }
}

fun main(args: Array<String>) {
    val t = Test()
    // 常规写法 传入函数
    t.doTest { test -> test.doSomething() }
    // 使用引用函数(Test::doSomething实际上是对lambda表达式{test -> test.doSomething()}的简化)
    t.doTest(Test::doSomething)
}

参数lambda化

代码语言:javascript复制
fun cal(a: Int, b: Int, f: (a: Int, b: Int) -> Int): Int {
    return f(a, b)
}

fun main(args: Array<String>) {
    val sum = { a: Int, b: Int -> a   b }
    val result = cal(2, 3, sum)
    println("result = $result")
    // result = 5
}

利用前面写的方式,将一个函数改写为lambda形式,作为参数直接赋值给cal函数。

那么更进一步,可以省略这个lambda的变量,直接将lambda表达式传入函数。

代码语言:javascript复制
fun cal(a: Int, b: Int, f: (a: Int, b: Int) -> Int): Int {
    return f(a, b)
}

fun main(args: Array<String>) {
    val result = cal(2, 3, { a: Int, b: Int -> a   b })
    println("result = $result")
    // result = 5
}

另外,在Kotlin中调用高阶函数时,如果最后一个参数为lambda表达式,可以将lambda表达式写在外面,而且如果没有其它参数的话,小括号也是可以省略的。

代码语言:javascript复制
fun cal(a: Int, b: Int, f: (a: Int, b: Int) -> Int): Int {
    return f(a, b)
}

fun main(args: Array<String>) {
    val result = cal(2, 3, { a: Int, b: Int -> a   b })
    // 两种写法等价
    val result2 = cal(2, 3) { a: Int, b: Int -> a   b }
    println("result = $result")
    // result = 5
}

函数变量

代码语言:javascript复制
fun main(args: Array) {
    val sumLambda = {a: Int, b: Int -> a   b}
    var numFun: (a: Int, b: Int) -> Int
    numFun = {a: Int, b: Int -> a   b}
    numFun = sumLambda
    numFun = ::sum
    numFun(1,2)
}

可以看到这个变量可以等于一个lambda表达式,也可以等于另一个lambda表达式变量,还可以等于一个普通函数,但是在函数名前需要加上(::)来获取函数引用。

lambda表达式实例

下面通过一个简单的例子来看下一些具体的lambda表达式是怎么写的。

代码语言:javascript复制
// 匿名函数
val sum = fun(a: Int, b: Int): Int {
    return a   b
}

// 具名函数
fun namedSum(a: Int, b: Int): Int {
    return a   b
}

// 高阶函数
fun highSum(a: Int, b: Int, f: (Int, Int) -> Int): Int {
    return f(a, b)
}

fun main(args: Array<String>) {
    // 通过()来执行匿名函数sum
    val add = sum(1, 2)
    println(add)
    // 通过lambda表达式来完成函数highSum
    val add2 = highSum(3, 4) { a, b -> a   b }
    println(add2)
    // 通过函数引用来完成函数highSum
    val add3 = highSum(5, 6, ::namedSum)
    println(add3)

    // forEach参数接收一个函数
    args.forEach({ it: String -> println(it) })
    // 去掉返回值,自动推断
    args.forEach({ it -> println(it) })
    // 只有一个参数的时候可以省略it
    args.forEach({ println(it) })
    // lambda表达式在最后一个参数可以外移
    args.forEach() { println(it) }
    // 函数若无参数可以去掉()
    args.forEach { println(it) }
    // 引用函数
    args.forEach(::println)
}

函数类型与实例化

类似Int、String这样的数据类型,函数的类型表示为:

代码语言:javascript复制
(Type1, Type2, ...) -> Type
// 例如
(Int) -> Int

// 所以才有了这样的函数
fun test(a: Int, f: (Int) -> Int): Int {
    return f(a)
}

其中(Int) -> Int的地位和Int、String的地位是等价的。

函数既然是一种类型,那么函数也和Int、String一样,是具有可实例化的实例的,例如Int的实例1、String的实例“xys”,那么获取函数的实例,主要客源通过下面三种方式:

  1. :: 双冒号操作符表示对函数的引用
  2. lambda表达式
  3. 匿名函数
代码语言:javascript复制
fun main(args: Array<String>) {
    // 引用函数
    println(test(1, 2, ::add))
    // 匿名函数
    val add = fun(a: Int, b: Int): Int {
        return a   b
    }
    println(test(3, 4, add))
    // lambda表达式
    println(test(5, 6, { a, b -> a   b }))// lambda作为最后一个参数可以提到括号外
    println(test(5, 6) { a, b -> a   b })
}

fun test(a: Int, b: Int, f: (Int, Int) -> Int): Int {
    return f(a, b)
}

fun add(a: Int, b: Int): Int {
    return a   b
}

lambda表达式的类型

通过下面的例子,可以了解下lambda表达式的类型,代码如下所示。

代码语言:javascript复制
// 无参,返回String
() -> String

// 两个整型参数,返回字符串类型
(Int, Int) -> String

// 传入了一个lambda表达式和一个整型,返回Int
(()->Unit, Int) -> Int

开发者可以通过类似上面的形式来表达lambda表达式的类型,不过和Int、String一样,lambda表达式也有自己的类,即Function类。

Kotlin封装了Function0到Function22,一共23个Function类型,分别表示参数个数从0到22。

lambda表达式的return

除非使用标签指定了返回点,否则return从最近的使用fun关键字声明的函数返回。

代码语言:javascript复制
fun main(args: Array<String>) {
    var sum: (Int) -> Unit = tag@{
        print("Test return $it")
        return@tag
    }
    sum(3)
}

SAM转换

SAM = Single Abstract Method,即唯一抽象方法

SAM转换是为了在Kotlin代码中调用Java代码所提供的一个语法糖,即为Java的单一方法的接口,提供lambda形式的实现,例如Android中最常见的view.setOnClickListener:

代码语言:javascript复制
// SAM convert in KT
view.setOnClickListener{
    view -> doSomething
}

// Java接口
public interface OnClickListener {
     void onClick(View v);
}

SAM转换是专门为Java提供的语法糖,用于将lambda表达式转换成相应的匿名类的实例。在Kotlin中实现相同的功能,只需要使用函数参数即可。

带接收者的lambda表达式

lambda表达式实际上有两种形式,一种是前面介绍的基本形式,还有一种是带接收者的形式,两种lambda表达式如下所示。

普通lambda表达式:{ () -> R }

即函数无入参,返回值为R类型。

带接收者的lambda表达式:{ T.() -> R }

即申明一个T类型的接收者对象,且无入参,返回值为R类型。

Kotlin中的拓展函数,实际上就是使用的带接收者的lambda表达式,

带接收者的lambda与普通的lambda的区别主要在于this的指向区别,T.() -> R里的this代表的是T的自身实例,而() -> R里,this代表的是外部类的实例。

使用typealias给重复申明的lambda表达式设置别名

代码语言:javascript复制
fun fun1(f: (Int) -> Unit) {
    f(1)
}

fun fun2(f: (Int) -> Unit) {
    f(2)
}

// 使用typealias
typealias intFun = (Int) -> Unit

fun fun3(f: intFun) {
    f(3)
}

fun fun4(f: intFun) {
    f(4)
}

fun main(args: Array<String>) {
    fun1 { println(it) }
    fun2 { println(it) }
    fun3 { println(it) }
    fun4 { println(it) }
}

闭包

如果一个函数内部申明或者返回了一个函数,那么这个函数被称之为闭包。

函数内部的变量可以被函数内部申明的函数所访问、修改,这就让闭包可以携带状态(所有的中间值都会被放入内存中)。

开发者可以通过闭包让函数具有状态,从而可以封装函数的状态,让函数具有面向对象的特性。

为什么需要闭包

在了解闭包之前,需要先了解下变量的作用域,在kotlin中,变量的作用域只有两种,即全局变量和局部变量。

  • 全局变量,函数内部和函数外部均可以直接访问。
  • 局部变量,只有函数内部可以访问。

那么如何在函数外部访问函数内部的局部变量呢,这就需要通过闭包来进行访问,闭包的设计就是为了能让开发者读取某个函数内部的变量。

所以闭包就是能够读取其它函数的局部变量的函数。

闭包让函数携带状态

代码语言:javascript复制
fun test(): () -> Int {
    var a = 1
    println(a)
    return fun(): Int {
        a  
        println(a)
        return a
    }
}

fun main(args: Array<String>) {
    val t = test()
    t()
    t()
}

// output
1
2
3

变量t的类型实际上是一个匿名函数,所以在调用t函数执行的时候,实际上执行的是返回的匿名函数,同时,由于闭包可以携带外包的变量值,所以a的状态值被传递了下来。

闭包可以访问函数体之外的变量,这被称之为变量捕获。闭包会将捕获的变量保存在一个特殊的内存区域,从而实现闭包携带状态的功能。

kotlin实现接口回调

单方法回调

代码语言:javascript复制
class Test {
    private var callBack: ((str: String) -> Unit)? = null
    fun setCallback(myCallBack: ((str: String) -> Unit)) {
        this.callBack = myCallBack
    }
}

使用函数替代了接口的实现。

回调写法的演进

Java思想的kotlin写法

代码语言:javascript复制
interface ICallback {
    fun onSuccess(msg: String)

    fun onFail(msg: String)
}

class TestCallback {

    var myCallback: ICallback? = null

    fun setCallback(callback: ICallback) {
        myCallback = callback
    }

    fun init() {
        myCallback?.onSuccess("success message")
    }
}

fun main(args: Array<String>) {
    val testCallback = TestCallback()
    testCallback.setCallback(object : ICallback {
        override fun onSuccess(msg: String) {
            println("success $msg")
        }

        override fun onFail(msg: String) {
            println("fail $msg")
        }
    })
    testCallback.init()
}

使用lambda表达式替代匿名内部类实现。

代码语言:javascript复制
class TestCallback {

    var mySuccessCallback: (String) -> Unit? = {}
    var myFailCallback: (String) -> Unit? = {}

    fun setCallback(successCallback: (String) -> Unit, failCallback: (String) -> Unit) {
        mySuccessCallback = successCallback
        myFailCallback = failCallback
    }

    fun init() {
        mySuccessCallback("success message")
        myFailCallback("fail message")
    }
}

fun main(args: Array<String>) {
    val testCallback = TestCallback()
    testCallback.setCallback({ println("success $it") }, { println("fail $it") })
    testCallback.init()
}

这样去掉了接口和匿名内部类。

高阶函数的使用场景

高阶函数的一个重要使用场景就是集合的操作,里面下面这个例子,分别使用Java和Kotlin实现了「找最大值」的方法。

代码语言:javascript复制
data class Test(val name: String, val age: Int)

fun main(args: Array<String>) {
    // Java写法
    val testList = listOf(Test("xys", 18), Test("qwe", 12), Test("rty", 10), Test("zxc", 2))
    findMax(testList)
    // Kotlin写法
    println(testList.maxBy { it.age })
    println(testList.maxBy(Test::age))
}

fun findMax(test: List<Test>) {
    var max = 0
    var currentMax: Test? = null
    for (t in test) {
        if (t.age > max) {
            max = t.age
            currentMax = t
        }
    }
    println(currentMax)
}

函数式集合操作

fliter & map

filter用于数据的筛选,类似的还有filterIndexed,即带Index的过滤器、filterNot,即过滤所有不满足条件的数据。

map用于对数据进行变换,代表了一种一对一的变换关系,它可以对集合中的数据做一次变换,类似的还有mapIndexed()。

代码语言:javascript复制
fun main(args: Array<String>) {
    val test = listOf(1, 3, 5, 7, 9)

    // filter函数遍历集合并选出应用给定lambda后会返回true的那些元素
    println("大于5的数 ${test.filter { it > 5 }}")
    // map函数对集合中的每一个元素应用给定的函数并把结果收集到一个新集合
    println("平方操作 ${test.map { it * it }}")

    val testList = listOf(Test("xys", 18), Test("qwe", 12), Test("rty", 10), Test("zxc", 2))
    // 将一个列表转换为另一个列表
    println("只展示name ${testList.map { it.name }}")
    // filter与map链式操作
    println("展示age大于10的name ${testList.filter { it.age > 10 }.map { it.name }}")
}

data class Test(val name: String, val age: Int)
all & any & count & find
代码语言:javascript复制
fun main(args: Array<String>) {
    val test = listOf(1, 3, 5, 7, 9)

    // all判断是否全部符合lambda表达式的条件
    println("是否全部符合>10 ${test.all { it > 10 }}")
    // any判断是否存在有符合lambda表达式的条件的数据
    println("是否存在>8 ${test.any { it > 8 }}")
    // count获取符合lambda表达式条件的数据个数
    println("大于5的个数 ${test.count { it > 5 }}")
    // find获取符合lambda表达式条件的第一个数据
    println("第一个大于5 ${test.find { it > 5 }}")
    println("最后一个大于5 ${test.findLast { it > 5 }}")
}
groupBy & partition & flatMap

flatMap()代表了一个一对多的关系,可以将每个元素变换为一个新的集合,再将其平铺成一个集合。

groupBy()方法会返回一个Map<k,list>的Map对象,其中Key就是我们分组的条件,value就是分组后的集合。</k,list

代码语言:javascript复制
fun main(args: Array<String>) {
    val test = listOf("a", "ab", "b", "bc")

    // groupBy按照lambda表达式的条件重组数据并分组
    println("按首字母分组 ${test.groupBy(String::first)}")
    // partition按照条件进行分组,该条件只支持Boolean类型条件,first为满足条件的,second为不满足的
    test.partition { it.length > 1 }.first.forEach { print("$it、") }
    println()
    test.partition { it.length > 1 }.second.forEach { print("$it、") }
    println()
    // flatMap首先按照lambda表达式对元素进行变换,再将变换后的列表合并成一个新列表
    println(test.flatMap { it.toList() })
}
sortedBy

sortedBy()用于根据指定的规则进行顺序排序,如果要降序排序,则需要使用sortedByDescending(),

代码语言:javascript复制
fun main(args: Array<String>) {
    val test = listOf(3, 2, 4, 6, 7, 1)
    println(test.sortedBy { it })
}
take & slice

take()和slice()用于进行数据切片,从某个集合中返回指定条件的新集合。类似的还有takeLast()、takeIf()等。

代码语言:javascript复制
fun main(args: Array<String>) {
    val test = listOf(3, 2, 4, 6, 7, 1)
    // 获取前3个元素的新切片
    println(test.take(3))
    // 获取指定index组成的新切片
    println(test.slice(IntRange(2, 4)))
}
reduce
代码语言:javascript复制
fun main(args: Array<String>) {
    val test = listOf("a", "ab", "b", "bc")

    // reduce函数将一个集合的所有元素通过传入的操作函数实现数据集合的累积操作效果。
    println(test.reduce { acc, name -> "$acc$name" })
}

作用域函数

前面文章提到的作用域函数就是高阶函数的一个重要实例。

lambda表达式的其它特性

惰性序列操作

当一些集合函数进行链式调用的时候,每个函数的调用结果都将保存为一个新的临时列表,因此,大量的链式操作会产生大量的中间变量,从而导致性能问题,为了提高效率,可以把链式操作改为序列(sequance)。

调用扩展函数asSequence把任意集合转换成序列,调用toList来做反向的转换

代码语言:javascript复制
fun main(args: Array<String>) {
    val testList = listOf(Test("xys", 18), Test("qwe", 12), Test("rty", 10), Test("zxc", 2))

    // 函数的链式调用
    println("集合调用 展示age大于10的name ${
    testList.filter { it.age > 10 }
            .map { it.name }}")
    // 函数的序列操作
    println("序列操作 展示age大于10的name ${
    testList.asSequence()
            .filter { it.age > 10 }
            .map { it.name }
            .toList()}")
}

data class Test(val name: String, val age: Int)

一个完整的序列包括两个操作,即中间序列和末端序列,中间序列操作始终都是惰性的,末端序列操作触发所有的惰性计算。

代码语言:javascript复制
                       //中间操作            //末端操作
testList.asSequence(). filter {..}.map {..}.toList()

因此,通过序列的这种方式,就避免了产生大量中间集合,从而提高了性能。

延迟计算

代码语言:javascript复制
fun main(args: Array<String>) {
    val addResult = lateAdd(2, 4)
    print(addResult())
}

fun lateAdd(a: Int, b: Int): Function0<Int> {
    fun add(): Int {
        return a   b
    }
    return ::add
}

在lateAdd内部定义了一个局部函数,最后返回了该局部函数的引用,对结果使用()操作符拿到最终的结果,达到延迟计算的目的。

代码语言:javascript复制
fun main(args: Array<String>) {
    val funs = mapOf("sum" to ::sum)
    val mapFun = funs["sum"]
    if (mapFun != null) {
        val result = mapFun(1, 2)
        println("sum result -> $result")
    }
}

fun sum(a: Int, b: Int): Int {
    return a   b
}

掌握了lambda表达式,就等于战士有了枪,多练习,多思考,才能掌握lambda表达式的精髓,为后面掌握函数式编程,打下坚实的基础。

0 人点赞