Kotlin知识概括(五) —— Lambda

前序

      在Kotlin中,函数做为一等公民存在,函数能够像值同样被传递。lambda就是将一小段代码封装成匿名函数,以参数值的方式传递到函数中,供函数使用。java

初识lambda

      在Java8以前,当外部须要设置一个类中某种事件的处理逻辑时,每每须要定义一个接口(类),并建立其匿名实例做为参数,具体的处理逻辑存放到某个对应的方法中来实现:android

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的返回值类型。如今一个函数必需的参数列表、函数体和返回类型都一一找出来了。bash

函数类型

      都说能够将函数做为变量值传递,那该变量的类型如何定义呢?函数变量的类型统称函数类型,所谓函数类型就是声明该函数的参数类型列表和函数返回值类型。闭包

先看个简单的函数类型:app

() -> Unit
复制代码

      函数类型和lambda同样,使用 -> 做分隔符,但函数类型是将参数类型列表和返回值类型分开,全部函数类型都有一个圆括号括起来的参数类型列表和返回值类型。ide

一些相对简单的函数类型:函数

//无参、无返回值的函数类型(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。post

      当显示声明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知识概括(十四) —— 反射

相关文章
相关标签/搜索