Kotlin lambda学习

本文详细介绍了Kotlin中的Lambda表达式,包括其定义、使用、在作用域中访问变量、成员引用、在集合操作中的应用,如filter、map、all、any、count和find,以及如何使用groupBy进行分组,利用flatMap和flatten处理嵌套集合。此外,还讨论了序列(惰性集合)的创建和使用,以及Kotlin Lambda如何与Java的函数式接口配合工作,展示了with和apply的用法,帮助读者深入理解Kotlin的函数式编程特性。
摘要由CSDN通过智能技术生成

Table of Contents

 

什么是 Lambda?

Lambda 使用

在作用域中访问变量

成员引用

集合中使用 lambda

filter 和 map

all、any、count 和 find

groupBy:将列表转为分组

flatMap 和 flatten:处理嵌套集合

序列

创建序列

和 java 一起工作的 lambda

将 lambda 手动转换为函数式接口

with 和 apply

小结


什么是 Lambda?

Lambda 简答来说就是一小段代码块,并且我们可以将这个代码块在函数之间传递,这是函数式编程的一个重要特性。通常我们会需要一个函数,但是又不想定义一个函数那么费事,这个时候就可以使用 lambda 表达式来完成工作。这就是 lambda 函数,概念清晰简单。

Java 8 中非常重要的一个特性就是引入了 lambda,这受到广大工程师的热烈欢迎。为什么 Lambda 如此受欢迎,下面我们来一一解释下。

Lambda 使用

让我们先看一个例子,在 java 的开发中,通常会见到下面的代码:

button.setOnClickListener(new OnClickListener() {
    @Override
    public void onClick(View view) {
        // 点击后执行的动作
    }
});

对于这样的代码,我们早已司空见惯,习以为常,觉得这样的代码没啥毛病。但是,如果你看到 Lambda 版本的代码的话,可能就不这么想了:

button.setOnClickListener {
    // 点击之后的操作
}

看到没,是不是清爽很多了?下面我们就来看下在 kotlin 中的 lambda 的语法:

{ x: Int, y: Int -> x + y }

上面的形式就是 kotlin 中定义个 lambda 的形式,有如下要点:

  1. lambda 始终被花括号包围
  2. 实参不用括号包围
  3. 使用箭头将参数列表和代码块分隔开

 对于如上定义的 lambda,我们在 kotlin 中可以如下方式来使用:

val sum = { x: Int, y: Int -> x + y }

我们可以定义个变量来保存 lambda,然后就像调用函数那样调用 lambda。另外你也可以直接调用 lambda:

println({ x: Int, y: Int -> x + y }(1, 2))

不要怀疑,确实可以这样使用,lambda 后面的括号在 kotlin 中被称为 invoke 机制。

接下来,我们看一个例子来学习一下 lambda 的更多用法。

假设我们需要重一个人员列表中找到年龄最大的那一位,在 kotlin 中使用 lambda 可以如下实现:

val personList = listOf(
            Person("Jack", 18),
            Person("Tom", 19),
            Person("Jim", 24))
val oldestPerson = personList.maxBy({ p: Person -> p.age })

这里我们借用 kotlin 系统提供的集合扩展函数 maxBy 来实现查找,通过查看 maxBy 函数的定义我们知道,我们需要给它传递一个 lambda 即可,因此上面我们给他传递一个 lambda,这个 lambda 的类型是:

(Person) -> Int

也即是将一个 Person 对象转换为 Int 类型,方便比较大小。因为我们是要找年龄最大的,因此我们只要将 Person 对象转为年龄值就行。

上面的调用虽然一目了然,但是确实有点啰嗦。首先过多的标点符号破坏了可读性,其次类型可以从上下文中推断出来,我们不用明显声明这是 Person 类型,最后这种情况下我们不需要给 lambda 分配一个参数名称,因为我们只有一个。

在kotlin 中有这样的约定,如果 lambda 表达式是函数的最后一个实参,他可以放到括号的外面,因此上面的调用简化如下:

val oldestPerson = personList.maxBy(){ p: Person -> p.age }

当 lambda 是唯一的实参时,连圆括号都可以省略:

val oldestPerson = personList.maxBy { p: Person -> p.age }

因为 lambda 表达式中的实参 Person 类型可以从 personList 中推断出来,因此我们还可以简化:

val oldestPerson = personList.maxBy { it.age }

这里使用 it 表达形式,it 是 person 对象在 lambda 中的迭代元素,可以简单理解为:it 就是 personList 中的每一个 person 对象的代表。

以上四种形式功能完成等同,但是最后一种可读性是最好的,是最具有 kotlin style 的。

但是如果你有两个以上的 lambda 参数需要传递,那么你只能把最后一个 lambda 放到括号外面。通常这种情况下,常规的语法形式是比较好的选择。

到目前位置,我们看到的 lambda 都是单语句的形式,但是 lambda 中可以有多个语句,这种情况下,最后一个表达式的值就是 lambda 的结果,比如:

val sum = { x: Int, y: Int ->
   println("hello world.")
   x + y
}

上面我们看到了 lambda 的基本用法,下面我们看下和 lambda 表达式息息相关的概念:从上下文中捕捉变量。

在作用域中访问变量

我们知道,lambda 的主要使用场景是在函数之间传递。假如我们将 lambda 作为参数传递给一个函数,然后我们在 lambda 中需要访问函数中定义的局部变量,例如参数,怎么办呢?在 kotlin 中,没关系,你可以直接访问。

为了说明,我们使用 kotlin 库函数中最常使用的 forEach 来展示这种行为,forEach 是最基本的集合操作函数之一,它需要一个 lambda 表达式,然后针对集合中的每一个元素都执行该表达式。下面是使用例子:

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

fun main() {
    val personList = listOf(
            Person("Jack", 18),
            Person("Tom", 19),
            Person("Jim", 24))

    printNames(personList, "Name")
}

fun printNames(personList: Collection<Person>, prefix: String) {
    personList.forEach {
        println("$prefix: ${it.name}")
    }
}

在这个例子中,我们可以看到 kotlin 和 java 的一个显著区别就是,在 kotlin 中不会仅限于访问 final 类型的变量,我们在 lambda 中可以自由地访问参数变量。同时 lambda 不仅可以访问,还可以修改函数的局部变量值:

fun printNames(personList: Collection<Person>, prefix: String) {
    var count = 0
    personList.forEach {
        count++
        println("$prefix: ${it.name} at $count")
    }
}

这里定义的非 final 变量 count,我们可以在 lambda 中自由访问,并且修改值。

从 lambda 中访问外部变量,在 kotlin 中称为 lambda 捕捉。在上面的例子中,forEach 中的 lambda 捕捉了 prefix 和 count 两个局部变量。

在默认情况下,函数局部变量的声明周期是限制在这个函数的内部的,一旦函数运行结束,这个变量在内存中也就是消失了。但是如果它被 lambda 捕捉了,使用这个变量的代码可以被存储并稍后在执行。这背后的原理很简单,就是被捕捉的值和 lambda 代码一起被存储了起来。对于 final 变量(val 类型)来说,他的值和使用的 lambda 一起存储起来;而对于非 final(var 类型)的变量来说,他的值被封装在一个特殊的包装器中,这样你就可以改变这个值,而这个包装器对象的引用就是一个 final 变量,它会和 lambda 一起被存储。

我们知道 kotlin 是可以运行在 jvm 上面的,因此在 jvm 这个层面上没有任何魔法。既然在 java 中不能在函数的匿名内部类对象访问非 final 类型的变量,那么 kotlin 是如何做到访问非 final 类型的变量呢?答案就在变量的包装类中,这个包装类的实现差不多如下:

class Ref<T> (var value: T)

使用这样的包装类将数据包装起来,然后将这个包装类的对象引用变为 final 类型的,这个时候我们就可以在兼容 jvm 的同时又可以访问非 final 类型的变量了。

需要注意的是,如果 lambda 被用作事件处理器或者用在其他异步执行的情况,对布局的修改只会在 lambda 执行的时候发生,因此我们不可以使用如下的代码来统计按钮被点击的次数:

fun countClick(button: Button): Int {
    var count = 0
    button.onClick { count++ }
    return count
}

因为这里的 count 的累加操作是按钮被点击的时候才会触发的,所以这个函数每次调用都会返回 0。

成员引用

我们已经看到 lambda 是如何让你把代码块作为参数传递给函数,但是我们想要用作参数传递的代码已经被定义成了函数,这个时候我们怎么办呢?当然,我们可以传递一个调用这个函数的 lambda,但是这样显得有点蠢。在 kotlin 中提供了类似 C 中的解决方案,我们可以类似 C 中那样,使用一个变量来保存函数名称,然后访问这个变量就可以来访问这个函数了。例如:</

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值