前序
在Kotlin中,函数作为一等公民存在,函数可以像值一样被传递。lambda就是将一小段代码封装成匿名函数,以参数值的方式传递到函数中,供函数使用。
初识lambda
在Java8之前,当外部需要设置一个类中某种事件的处理逻辑时,往往需要定义一个接口(类),并创建其匿名实例作为参数,具体的处理逻辑存放到某个对应的方法中来实现:
mName.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
}
});
但Kotlin说,太TM啰嗦了,我直接将处理逻辑(代码块)传递给你:
mName.setOnClickListener {
}
上面的语法为Kotlin的lambda表达式,都说lambda是匿名函数,匿名是知道了,但参数列表和返回类型呢?那如果这样写呢:
val sum = { x:Int, y:Int ->
x + y
}
lambda表达式始终用花括号包围,并用 -> 将参数列表和函数主体分离。当lambda自行进行类型推导时,最后一行表达式返回值类型作为lambda的返回值类型。现在一个函数必需的参数列表、函数体和返回类型都一一找出来了。
函数类型
都说可以将函数作为变量值传递,那该变量的类型如何定义呢?函数变量的类型统称函数类型,所谓函数类型就是声明该函数的参数类型列表和函数返回值类型。
先看个简单的函数类型:
() -> Unit
函数类型和lambda一样,使用 -> 作分隔符,但函数类型是将参数类型列表和返回值类型分开,所有函数类型都有一个圆括号括起来的参数类型列表和返回值类型。
一些相对简单的函数类型:
//无参、无返回值的函数类型(Unit 返回类型不可省略)
() -> Unit
//接收T类型参数、无返回值的函数类型
(T) -> Unit
//接收T类型和A类型参数、无返回值的函数类型(多个参数同理)
(T,A) -> Unit
//接收T类型参数,并且返回R类型值的函数类型
(T) -> R
//接收T类型和A类型参数、并且返回R类型值的函数类型(多个参数同理)
(T,A) -> R
较复杂的函数类型:
(T,(A,B) -> C) -> R
一看有点复杂,先将(A,B) -> C抽出来,当作一个函数类型Y,Y = (A,B) -> C,整个函数类型就变成(T,Y) -> R。
当显示声明lambda的函数类型时,可以省去lambda参数列表中参数的类型,并且最后一行表达式的返回值类型必须与声明的返回值类型一致:
val min:(Int,Int) -> Int = { x,y ->
//只能返回Int类型,最后一句表达式的返回值必须为Int
//if表达式返回Int
if (x < y){
x
}else{
y
}
}
挂起函数属于特殊的函数类型,挂起函数的函数类型中拥有 suspend 修饰符 ,例如 suspend () -> Unit 或者 suspend A.(B) -> C。(挂机函数属于协程的知识,可以暂且放过)
类型别名
类型别名为现有类型提供替代名称。如果类型名称太长,可以另外引入较短的名称,并使用新的名称替代原类型名。类型别名不会引入新类型,它等效于相应的底层类型。使用类型别名为函数类型起别称:
typealias alias = (String,(Int,Int) -> String) -> String
typealias alias2 = () -> Unit
除了函数类型外,也可以为其他类型起别名:
typealias FileTable<K> = MutableMap<K, MutableList<File>>
lambda语句简化
由于Kotlin会根据上下文进行类型推导,我们可以使用更简化的lambda,来实现更简洁的语法。以maxBy函数为例,该函数接受一个函数类型为(T) -> R的参数:
data class Person(val age:Int,val name:String)
val persons = listOf(Person(17,"daqi"),Person(20,"Bob"))
//寻找年龄最大的Person对象
//花括号的代码片段代表lambda表达式,作为参数传递到maxBy()方法中。
persons.maxBy( { person: Person -> person.age } )
- 当lambda表达式作为函数调用的最后一个实参,可以将它放在括号外边:
persons.maxBy() { person: Person ->
person.age
}
persons.joinToString (" "){person ->
person.name
}
- 当lambda是函数唯一的实参时,还可以将函数的空括号去掉:
persons.maxBy{ person: Person ->
person.age
}
- 跟局部变量一样,lambda参数的类型可以被推导处理,可以不显式的指定参数类型:
persons.maxBy{ person ->
person.age
}
因为maxBy()函数的声明,参数类型始终与集合的元素类型相同,编译器知道你对Person集合调用maxBy函数,所以能推导出lambda表达式的参数类型也是Person。
public inline fun <T, R : Comparable<R>> Iterable<T>.maxBy(selector: (T) -> R): T? {
}
但如果使用函数存储lambda表达式,则无法根据上下文推导出参数类型,这时必须显式指定参数类型。
val getAge = { p:Person -> p.age }
//或显式指定变量的函数类型
val getAge:(Person) -> Int = { p -> p.age }
- 当lambda表达式中只有一个参数,没有显示指定参数名称,并且这个参数的类型能推导出来时,会生成默认参数名称it
persons.maxBy{
it.age
}
默认参数名称it虽然简洁,但不能滥用。当多个lambda嵌套的情况下,最好显式地声明每个lambda表达式的参数,否则很难搞清楚it引用的到底是什么值,严重影响代码可读性。
var persons:List<Person>? = null
//显式指定参数变量名称,不使用it
persons?.let { personList ->
personList.maxBy{ person ->
person.age
}
}
- 可以把lambda作为命名参数传递
persons.joinToString (separator = " ",transform = {person ->
person.name
})
- 当函数需要两个或以上的lambda实参时,不能把超过一个的lambda放在括号外面,这时使用常规传参语法来实现是最好的选择。
SAM 转换
回看刚开始的setOnClickListener()方法,那接收的参数是一个接口实例,不是函数类型呀!怎么就可以传lambda了呢?先了解一个概念:函数式接口:
函数式接口就是只定义一个抽象方法的接口
SAM转换就是将lambda显示转换为函数式接口实例,但要求Kotlin的函数类型和该SAM(单一抽象方法)的函数类型一致。SAM转换一般都是自动发生的。
SAM构造方法是编译器为了将lambda显示转换为函数式接口实例而生成的函数。SAM构造函数只接收一个参数 —— 被用作函数式接口单抽象方法体的lambda,并返回该函数式接口的实例。
SAM构造方法的名称和Java函数式接口的名称一样。
显示调用SAM构造方法,模拟转换:
#daqiInterface.java
//定义Java的函数式接口
public interface daqiInterface {
String absMethod();
}
#daqiJava.java
public class daqiJava {
public void setDaqiInterface(daqiInterface listener){
}
}
#daqiKotlin.kt
//调用SAM构造方法
val interfaceObject = daqiInterface {
//返回String类型值
"daqi"
}
//显示传递给接收该函数式接口实例的函数
val daqiJava = daqiJava()
//此处不会报错
daqiJava.setDaqiInterface(interfaceObject)
对interfaceObject进行类型判断:
if (interfaceObject is daqiInterface){
println("该对象是daqiInterface实例")
}else{
println("该对象不是daqiInterface实例")
}
当单个方法接收多个函数式接口实例时,要么全部显式调用SAM构造方法,要么全部交给编译器自行转换:
#daqiJava.java
public class daqiJava {
public void setDaqiInterface2(daqiInterface listener,Runnable runnable){
}
}
#daqiKotlin.kt
val daqiJava = daqiJava()
//全部交由编译器自行转换
daqiJava.setDaqiInterface2( {"daqi"} ){
}
//全部手动显式SAM转换
daqiJava.setDaqiInterface2(daqiInterface { "daqi" }, Runnable { })
注意:
- SAM转换只适用于接口,不适用于抽象类,即使这些抽象类也只有一个抽象方法。
- SAM转换 只适用于操作Java类中接收Java函数式接口实例的方法。因为Kotlin具有完整的函数类型,不需要将函数自动转换为Kotlin接口的实现。因此,需要接收lambda的作为参数的Kotlin函数应该使用函数类型而不是函数式接口。
带接收者的lambda表达式
目前讲到的lambda都是普通lambda,lambda中还有一种类型:带接收者的lambda。
带接受者的lambda的类型定义:
A.() -> C
表示可以在A类型的接收者对象上调用并返回一个C类型值的函数。
带接收者的lambda好处是,在lambda函数体可以无需任何额外的限定符的情况下,直接使用接收者对象的成员(属性或方法),亦可使用this访问接收者对象。
似曾相识的扩展函数中,this关键字也执行扩展类的实例对象,而且也可以被省略掉。扩展函数某种意义上就是带接收者的函数。
扩展函数和带接收者的lambda极为相似,双方都需要一个接收者对象,双方都可以直接调用该对象的成员。如果将普通lambda当作普通函数的匿名方式来看看待,那么带接收者类型的lambda可以当作扩展函数的匿名方式来看待。
Kotlin的标准库中就有提供带接收者的lambda表达式:with和apply
val stringBuilder = StringBuilder()
val result = with(stringBuilder){
append("daqi在努力学习Android")
append("daqi在努力学习Kotlin")
//最后一个表达式作为返回值返回
this.toString()
}
//打印结果便是上面添加的字符串
println(result)
with函数,显式接收接收者,并将lambda最后一个表达式的返回值作为with函数的返回值返回。
查看with函数的定义:
public inline fun <T, R> with(receiver: T, block: T.() -> R): R {
}
其lambda的函数类型表示,参数类型和返回值类型可以为不同值,也就是说可以返回与接收者类型不一致的值。
apply函数几乎和with函数一模一样,唯一区别是apply始终返回接收者对象。对with的代码进行重构:
val stringBuilder = StringBuilder().apply {
append("daqi在努力学习Android")
append("daqi在努力学习Kotlin")
}
println(stringBuilder.toString())
查看apply函数的定义:
public inline fun <T> T.apply(block: T.() -> Unit): T {
}
函数被声明为T类型的扩展函数,并返回T类型的对象。由于其泛型的缘故,可以在任何对象上使用apply。
apply函数在创建一个对象并需要对其进行初始化时非常有效。在Java中,一般借助Builder对象。
lambda表达式的使用场景
- 场景一:lambda和集合一起使用,是lambda最经典的用途。可以对集合进行筛选、映射等其他操作。
val languages = listOf("Java","Kotlin","Python","JavaScript")
languages.filter {
it.contains("Java")
}.forEach{
println(it)
}
- 场景二:替代函数式接口实例
//替代View.OnClickListener接口
mName.setOnClickListener {
}
//替代Runnable接口
mHandler.post {
}
- 场景三:需要接收函数类型变量的函数
//定义函数
fun daqi(string:(Int) -> String){
}
//使用
daqi{
}
有限返回
前面说lambda一般是将lambda中最后一个表达式的返回值作为lambda的返回值,这种返回是隐式发生的,不需要额外的语法。但当多个lambda嵌套,需要返回外层lambda时,可以使用有限返回。
有限返回就是带标签的return
标签一般是接收lambda实参的函数名。当需要显式返回lambda结果时,可以使用有限返回的形式将结果返回。例子:
val array = listOf("Java","Kotlin")
val buffer = with(StringBuffer()) {
array.forEach { str ->
if (str.equals("Kotlin")){
//返回添加Kotlin字符串的StringBuffer
return@with this.append(str)
}
}
}
println(buffer.toString())
lambda表达式内部禁止使用裸return,因为一个不带标签的return语句总是在用fun关键字声明的函数中返回。这意味着lambda表达式中的return将从包含它的函数返回。
fun main(args: Array<String>) {
StringBuffer().apply {
//打印第一个daqi
println("daqi")
return
}
//打印第二个daqi
println("daqi")
}
结果是:第一次打印完后,便退出了main函数。
匿名函数
lambda表达式语法缺少指定函数的返回类型的能力,当需要显式指定返回类型时,可以使用匿名函数。匿名函数除了名称省略,其他和常规函数声明一致。
fun(x: Int, y: Int): Int {
return x + y
}
与lambda不同,匿名函数中的return是从匿名函数中返回。
lambda变量捕捉
在Java中,当函数内声明一个匿名内部类或者lambda时候,匿名内部类能引用这个函数的参数和局部变量,但这些参数和局部变量必须用final修饰。Kotlin的lambda一样也可以访问函数参数和局部变量,并且不局限于final变量,甚至能修改非final的局部变量!Kotlin的lambda表达式是真正意思上的闭包。
fun daqi(func:() -> Unit){
func()
}
fun sum(x:Int,y:Int){
var count = x + y
daqi{
count++
println("$x + $y +1 = $count")
}
}
正常情况下,局部变量的生命周期都会被限制在声明该变量的函数中,局部变量在函数被执行完后就会被销毁。但局部变量或参数被lambda捕捉后,使用该变量的代码块可以被存储并延迟执行。这是为什么呢?
当捕捉final变量时,final变量会被拷贝下来与使用该final变量的lambda代码一起存储。而对于非final变量会被封装在一个final的Ref包装类实例中,然后和final变量一样,和使用该变量lambda一起存储。当需要修改这个非final引用时,通过获取Ref包装类实例,进而改变存储在该包装类中的布局变量。所以说lambda还是只能捕捉final变量,只是Kotlin屏蔽了这一层包装。
查看源码:
public static final void sum(final int x, final int y) {
//创建一个IntRef包装类对象,将变量count存储进去
final IntRef count = new IntRef();
count.element = x + y;
daqi((Function0)(new Function0() {
public Object invoke() {
this.invoke();
return Unit.INSTANCE;
}
public final void invoke() {
//通过包装类对象对内部的变量进行读和修改
int var10001 = count.element++;
String var1 = x + " + " + y + " +1 = " + count.element;
System.out.println(var1);
}
}));
}
注意: 对于lambda修改局部变量,只有在该lambda表达式被执行的时候触发。
成员引用
lambda可以将代码块作为参数传递给函数,但当我需要传递的代码已经被定义为函数时,该怎么办?难不成我写一个调用该函数的lambda?Kotlin和Java8允许你使用成员引用将函数转换成一个值,然后传递它。
成员引用用来创建一个调用单个方法或者访问单个属性的函数值。
data class Person(val age:Int,val name:String)
fun daqi(){
val persons = listOf(Person(17,"daqi"),Person(20,"Bob"))
persons.maxBy({person -> person.age })
}
Kotlin中,当你声明属性的时候,也就声明了对应的访问器(即get和set)。此时Person类中已存在age属性的访问器方法,但我们在调用访问器时,还在外面嵌套了一层lambda。使用成员引用进行优化:
data class Person(val age:Int,val name:String)
fun daqi(){
val persons = listOf(Person(17,"daqi"),Person(20,"Bob"))
persons.maxBy(Person::age)
}
成员引用由类、双冒号、成员三个部分组成:
顶层函数和扩展函数都可以使用成员引用来表示:
//顶层函数
fun daqi(){
}
//扩展函数
fun Person.getPersonAge(){
}
fun main(args: Array<String>) {
//顶层函数的成员引用(不附属于任何一个类,类省略)
run(::daqi)
//扩展函数的成员引用
Person(17,"daqi").run(Person::getPersonAge)
}
还可以对构造函数使用成员引用来表示:
val createPerson = ::Person
val person = createPerson(17,"daqi")
Kotlin1.1后,成员引用语法支持捕捉特定实例对象上的方法引用:
val personAge = Person(17,"name")::age
lambda的性能优化
自Kotlin1.0起,每一个lambda表达式都会被编译成一个匿名类,带来额外的开销。可以使用内联函数来优化lambda带来的额外消耗。
所谓的内联函数,就是使用inline修饰的函数。在函数被使用的地方编译器并不会生成函数调用的代码,而是将函数实现的真实代码替换每一次的函数调用。Kotlin中大多数的库函数都标记成了inline。
参考资料:
- 《Kotlin实战》
- Kotlin官网