kotlin实战教程之lambda编程

前言

ambda即lambda表达式,简称lambda。本质上是可以传递给其它函数的一小段代码。有了lambda,可以轻松地把通用代码结构抽取成库函数。lambda最常见的用途是和集合一起配合。kotlin甚至还拥有带接收者的lambda,这是一种特殊的lambda。

本文是对<<kotlin实战>>中 “lambda编程”一章的总结,主要记录了一些我认为比较重要的点

在kotlin中常见的lambda用法主要由以下几种:

  • 与集合一起使用
  • lambda可以与任意java库一起使用
  • 带接收者的lambda,比如with和apply

lambda表达式的基本语法

下面是一个lambda表达式的基本语法:

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

lambda表达式始终用花括号包围,实参并没有用括号括起来。箭头把实参列表和lambda的函数体隔开

lambda作为函数的参数传递

可以把lambda表达式存储在一个变量中,把这个变量当做普通函数对待,也可以直接写作函数参数,比如有一个intOperator函数, 这个函数接收两个int参数,和一个函数。

fun intOperator(o1: Int, o2: Int, run: (a: Int, b: Int) -> Int) {
 run(o1, o2)
}

val sumLambda = { x: Int, y: Int -> x + y }
intOperator(1, 2, sumLambda)
intOperator(1, 2, {x:Int, y:Int -> x + y})

上面可以看到,我们直接把lambda当做一个函数传递个intOperator()作为参数。

如果lambda表达式是函数调用的最后一个实参,它可以放到括号外边:

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

如果lambda表达式是函数的唯一实参时,还可以去掉调用代码中的空括号对

比如下面这个例子

fun myPrint(lambda: () -> Unit) {
 lambda()
}

myPrint{
 print("a")
}

省略lambda参数类型并使用默认参数名称

在kotlin中如果lambda参数的类型可以被推导出来,我们就不需要显示声明它,比如我们常用的库函数 map:

listOf("1", "2", "3").map{
 //
}

在这个代码中使用了默认参数使用it代替。在kotlin中,如果当前上下文期望的是只有一个参数的lambda且这个参数的类型可以推断出来,就会生成这个名称。

允许在lambda内部访问非final变量甚至修改他们

在java中我们是知道的:匿名内部类不能访问非final变量,但在kotlin中可以:

fun main(args: Array<String>) {
 var count = 0
 listOf("1", "2", "3").forEach{
 count++
 }
 print(count)
}

其实对于kotlin来说,如果在lambad中引用非final变量,它的值会被封装起来,并且会和lambda代码一块存储。

当然对于异步代码或者事件响应回调这个是无效的。

成员引用

在上面我们知道可以直接把lambda当做函数的参数传递给一个函数,但是如果当做参数传递的代码已经被定义成了函数那怎么办呢?

在kotlin中可以使用::把函数转换成一个值,从而传递给函数。这里比如有一个Person类,他有一个say函数,我们可以这样获得这个函数的引用:

val sayQuote = Person::say

这种表达式叫做成员引用,对于顶层函数可以直接 ::say,来获得这个函数的引用。

常用的库函数

对于集合,kotlin提供了丰富的库函数便于我们使用,对于这些函数这里我们只介绍一些关键点。

filter与map

filter函数会遍历集合并选出应用给定lambda后会返回true的那些元素, 需要注意的是,返回的是一个新的集合

val newList = listOf(1, 2, 3, 4).filter{ it % 2 == 0}

map函数对集合中的每一个元素应用给定的函数并把结果收集到一个新集合中

val newList = listOf(1, 2, 3, 4).map{ it.toSting() }

all、any、count、find

  • all与any用来检查集合中的所有元素是否都符合某个条件, all表示全部 && any表示至少有一个
  • count函数检查有多少个元素满足判断式
  • find函数返回第一个符合条件的元素

count 与 size

在一些情况下使用count要高效于size, 比如统计集合中有多少个偶数:

listOf(1, 2, 3, 4, 5).count({it % 2 == 0})
listOf(1, 2, 3, 4, 5).filter({it % 2 == 0}).size

上面两种做法都可以实现这个需求,不过filter会创建一个新的集合,而 count方法只会跟踪匹配元素的数量,不关心元素本身。

其他还有 groupBy/flatMap/flatten,这里不细讲了。

惰性集合操作 : 序列

在说什么是惰性集合操作之前,我们先来看一下非惰性集合操作map与filter, 以获取姓名为A开头的人的名字为例:

peoples.map{it.name}.filter{it.startWith("A")}

我们要知道filter和map都会返回一个列表来保存结果,如果peoples这个集合元素非常多的话,那产生的这个中间集合就非常大,并且这个链式调用会非常低效。

为了解决这个问题kotlin引入了 惰性集合:序列, 序列中的元素的求值是惰性的,不需要创建集合来保存中间结果,我们可以使用序列来解决上面的问题:

peoples.asSequence().map{it.name}.filter{it.startWith("A")}.toList()

序列的操作

序列的操作分为两类:中间和末端操作, 以上面那个例子为例:

peoples.asSequence().map{it.name}.filter{it.startWith("A")}.toList()

map、filter都是中间操作,toList为末端操作。一次中间操作返回的是另一个序列,这个新序列知道如何变换原始序列中的元素,而一次末端操作返回的是一个结果,这个结果可能是集合、元素、数字等。

序列中中间操作的计算都是由末端操作触发的。

我们可以使用扩展函数asSequence把任意集合转换成序列,调用toList来做反向转换

我们来对比一下上面两种方法:

惰性集合.png

可以看到,使用序列会明显比直接使用map和filter来完成这个任务效率更高

  • 对原集合只会进行一次遍历
  • 只会生成一个结果集合
  • 对于混合有any这种集合操作,序列可以明显提高性能。

注意对于混合map/filter,这种操作时,如果被操作集合比较小,是不需要使用序列的。至于序列如何手动创建,这里不做细究

kotlin与Java函数式接口

函数式接口是指带有一个抽象方法的接口,在java api中比如Runnable、Callable等

我们在实际使用kotlin时,可能大部分API还是java API,但是kotlin的lambda可以无缝地和javaAPI互操作,比如给一个button设置onclick事件:

button.setOnClickListener{ //... }

这个操作在java8之前我们不得不通过创建一个匿名内部类来实现。

lambda表达式的可重用性

比如有一个函数postponeComputation(),接收一个函数,并循环执行这个函数指定次数:

postponeComputation(1000, object:Runnable{
 override fun run(){
 print(42)
 }
})

当你显示声明这个参数对象时,每次调用都会创建一个新的实例,而使用lambda情况不同:如果lambda没有访问任何来自自定义它的函数的变量,相应的匿名类实例可以在多次调用中重用:

 postponeComputation(1000, { print(42) })

但是如果lambda从包围它的作用域中捕捉了变量,每次调用就不再可能重用同一个实例了。 至于为什么将会在 Lambda的实现细节的讲到。

Lambda的实现细节

在kotlin中,每个函数式接口的lambda都会被编译成一个匿名类(除内联lambda)。如果lambda捕捉了变量,每个被捕捉的变量会在匿名内部类中有对应的字段,而且每次调用这个lambda都会创建一个这个匿名内部类的实例。如果没有捕捉变量,就会创建一个单例的类。

编译后的匿名内部类的名称由lambda声明所在的函数名称加上后缀衍生出来的,比如下面这个lambda:

class Person{
 fun test(){
 a.setRunnable({
  print("a")
 })
 }
}

这个lambda会被编译成:

class Person$1:Runnable{
 override fun run(){
 print("a")
 }
}

lambda与函数式接口的转换

有些时候我们需要函数式接口的实例,比如一个方法返回的是一个函数式接口,这时候就不能直接返回一个lambda了:

fun getRunnable():Runnable{}

这时候如果直接这样写就会报错 : fun getRunnable() = { } ,这是因为编译器不会智能转换,不过kotlin提供了 函数式接口构造方法来使操作更方便:

fun getRunnable() = Runnable{ }

Runnable{}是编译器生成的方法,等同于使用匿名对象的方式。

带接收者的lambda: with 与 apply

这两个函数式kotlin标准库中的函数。带接受者是指:在lambda函数体可以调用一个不同对象的方法,而且无须借助任何额外限定符。

with

with是一个接收两个参数的函数,一个参数是 被接收者, 它会被传给第二个参数 lambda表达式 , 在lambda表达式着呢个我们可以不用任何限定符直接访问这个值的方法和属性

fun alphabet():String{
 val stringBuilder = StringBuilder()
 return with(stringBuilder){
 for(letter in 'A'..'Z'){
  append(letter) //也可以使用this.append()
 }
 toString()
 }
}

with的返回值是执行了lambda代码的结果

apply

apply与with的唯一区别是它始终返回接收者对象。上面的函数我们可以这样改写:

fun alphabet() = StringBuilder().apply{
 for(letter in 'A'..'Z'){
 append(letter) //也可以使用this.append()
 }
}.toString()

内联函数:消除Lambda带来的运行时开销

上面我们已经知道,lambda表达式会被正常地编译成匿名类,这表示每调用一次lambda表达式,一个额外的类就会被创建,为了解决这个运行时性能的开销,kotlin提供了inline修饰符,如果使用inline

修饰符标记一个函数,在函数被使用的时候编译器并不会生成函数调用的代码,而是使用函数实现的真实代码替换每一次的函数调用。

先来举一个例子:

inline fun test(action:()->T){
 action()
}

fun foo(){
 test{
 print("a")
 }
}

foo()实际会被编译为下面的代码:

fun foo_(){
 print("a")
}

从上面这个例子可以看出,作为参数的lambda表达式会被直接替换到最终生成的代码中,而不是被包含在一个实现了函数接口的匿名类中。

注意如果lambda参数在某个地方被保存起来,以便后面可以继续使用,这种lambda表达式将不会被内联,因为必须要有一个包含这些代码的对象存在

内联的集合操作

kotlin标准库中的map、filter等大部分函数都是内联函数,因此使用标准库函数不需要担心性能开销。

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对我们的支持。

(0)

相关推荐

  • 玩转Kotlin 彻底弄懂Lambda和高阶函数

    Lambda是什么 简单来讲,Lambda是一种函数的表示方式(言外之意也就是说一个Lambda表达式等于一个函数).更确切的说:Lambda是一个未声明的函数,会以表达式的形式传递 为什么要用Lambda 设想一下,在Android中实现一个View的点击事件,可以使用如下实现: View view = findViewById(R.id.textView); view.setOnClickListener(new View.OnClickListener() { @Override publ

  • Kotlin基础学习之lambda中return语句详解

    前言 当我们爱上lambda并且大范围使用它的时候,我想大家都会被lambda中的return语句狠狠地调戏过,所以今天我们需要一起来揭开lambda中return的神秘面纱. 首先来看一个例子: fun demo() { val indexes = arrayOf(1, 2, 3, 4, 5, 6, 7) indexes.forEach { if (it > 5) { return } println(it) } println("End") } 按照我们的预期,调用demo后

  • 详解Kotlin 高阶函数 与 Lambda 表达式

    详解Kotlin 高阶函数 与 Lambda 表达式 高阶函数(higher-order function)是一种特殊的函数, 它接受函数作为参数, 或者返回一个函数. 这种函数的一个很好的例子就是 lock() 函数, 它的参数是一个锁对象(lock object), 以及另一个函数, 它首先获取锁, 运行对象函数, 然后再释放锁: fun <T> lock(lock: Lock, body: () -> T): T { lock.lock() try { return body()

  • kotlin实战教程之lambda编程

    前言 ambda即lambda表达式,简称lambda.本质上是可以传递给其它函数的一小段代码.有了lambda,可以轻松地把通用代码结构抽取成库函数.lambda最常见的用途是和集合一起配合.kotlin甚至还拥有带接收者的lambda,这是一种特殊的lambda. 本文是对<<kotlin实战>>中 "lambda编程"一章的总结,主要记录了一些我认为比较重要的点 在kotlin中常见的lambda用法主要由以下几种: 与集合一起使用 lambda可以与任意

  • Kotlin基础教程之Run,标签Label,函数Function-Type

    Kotlin基础教程之Run,标签Label,函数Function-Type 在Java中可以使用{}建立一个匿名的代码块,代码块会被正常的执行,除了改变了作用域之外,似乎并没有什么其他的作用.然而在Kotlin中却不能这么做,这是为什么呢? 其实,我们都知道一个函数一定与一个内存地址相关,而一个匿名的代码块其实也相当于是一个匿名的函数.在Kotlin中一般使用run函数来运行一段匿名代码块. 如下: 在Kotlin中使用标识符后跟@符号来定义一个标签,使用@后跟标识符来引用一个标签,run函数

  • Kotlin基础教程之dataclass,objectclass,use函数,类扩展,socket

    Kotlin基础教程之dataclass,objectclass,use函数,类扩展,socket Kotlin提供了一些机制来扩展已有的类,如下: 还记得我们之前写过的Point3D类吗?(将其略作修改,将成员变量改为Double类型) 让我们为其扩展一个length函数 扩展的方法很简单,只要在函数名前面加上类名就行了. 这样Point3D的对象就有了一个名为length的方法. 运行的结果不出所料: 除此之外,在Kotlin中还有一些特殊的类,比如Data Class: 有些类只包含数据,

  • vue3实战教程之axios的封装和环境变量

    目录 axios 基本使用 配置 封装 请求时添加loading 环境变量 总结 axios axios: ajax i/o system. 一个可以同时在浏览器和node环境进行网络请求的第三方库 功能特点: 在浏览器中发送 XMLHttpRequests 请求 在 node.js 中发送 http请求 支持 Promise API 拦截请求和响应 转换请求和响应数据 等等 基本使用 get请求 // 导入的axios是一个实例对象 import axios from 'axios' // a

  • 掌握SQL Server实战教程之SQL Server的安装指南

    目录 前言 一. 数据库的介绍 1.1 数据库的分类 1.2 MS SQL介绍 二. MS SQL的安装 2.1 从网站下载安装包 2.2 开始安装 选择基本版本 2.3 安装SSMS 三. 连接数据库 3.1 数据库的连接 3.2 创建数据库 总结 前言 本文沐风晓月带你来了解一下sqlserver数据库的安装及简单使用,本文的主要任务: 安装SQL Server并能够成功的远程链接,然后执行几条简单的SQL语句进行测试即可 一. 数据库的介绍 1.1 数据库的分类 数据库的种类有很多,根据存

  • java8学习教程之lambda表达式的使用方法

    前言 我们在 上一篇文章 中介绍了 lambda 表达式的语法,引入了 lambda 表达式的使用场景,以及使用 lambda 表达式的好处.我们将在这篇文章中,已实例讲解如何定义和使用 lambda 表达式,以及与其它语言相比,lambda 表达式在 Java 中的特殊规范. 使用匿名内部类的例子 首先明确一点,在 Java8 出现之前,lambda 表达式能够做到的,使用内部类也能做到,lambda 表达式只是简化了编程. 下面的例子是从列表中根据条件挑选出读者. 定义 TantanitRe

  • Java8学习教程之lambda表达式语法介绍

    前言 相信大家都知道,在Java8 中引入了 lambda 表达式,从行为参数化的角度,在使用时,将行为作为参数,去除包围在外层的不必要的类声明,使代码更加简洁. lambda 表达式的语法 lambda 表达式由参数,->,以及函数体三部分组成.其实函数体可以是表达式,也可以是语句.语句应该包含在{} 里,而表达式不能. lambda 表达式举例 (List<String> list) -> list.isEmpty() // 布尔类型表达式 () -> new Apple

  • python基础教程之lambda表达式使用方法

    Python中,如果函数体是一个单独的return expression语句,开发者可以选择使用特殊的lambda表达式形式替换该函数: 复制代码 代码如下: lambda parameters: expression lambda表达式相当于函数体为单个return语句的普通函数的匿名函数.请注意,lambda语法并没有使用return关键字.开发者可以在任何可以使用函数引用的位置使用lambda表达式.在开发者想要使用一个简单函数作为参数或者返回值时,使用lambda表达式是很方便的.下面是

  • MySQL实战教程之Join语句执行流程

    目录 Join语句执行流程 一.Index Nested-Loop Join 二.Simple Nested-Loop Join 三.Block Nested-Loop Join 四.总结 Join语句执行流程 Hi,我是阿昌,今天学习记录的是关于Join语句执行流程的内容. 在实际生产中,关于 join 语句使用的问题,一般会集中在以下两类: 不让使用 join,使用 join 有什么问题呢? 如果有两个大小不同的表做 join,应该用哪个表做驱动表呢? 创建两个表 t1 和 t2 来说明.

  • spring boot实战教程之shiro session过期时间详解

    前言 众所周知在spring boot内,设置session过期时间只需在application.properties内添加server.session.timeout配置即可.在整合shiro时发现,server.session.timeout设置为7200,但未到2小时就需要重新登录,后来发现是shiro的session已经过期了,shiro的session过期时间并不和server.session.timeout一致,目前是采用filter的方式来进行设置. ShiroSessionFil

随机推荐