阅读 455

Kotlin知识归纳(五) —— Lambda

前序

      在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。

参考资料:

android Kotlin系列:

Kotlin知识归纳(一) —— 基础语法

Kotlin知识归纳(二) —— 让函数更好调用

Kotlin知识归纳(三) —— 顶层成员与扩展

Kotlin知识归纳(四) —— 接口和类

Kotlin知识归纳(五) —— Lambda

Kotlin知识归纳(六) —— 类型系统

Kotlin知识归纳(七) —— 集合

Kotlin知识归纳(八) —— 序列

Kotlin知识归纳(九) —— 约定

Kotlin知识归纳(十) —— 委托

Kotlin知识归纳(十一) —— 高阶函数

Kotlin知识归纳(十二) —— 泛型

Kotlin知识归纳(十三) —— 注解

Kotlin知识归纳(十四) —— 反射

关注下面的标签,发现更多相似文章
评论