Kotlin知识归纳(五) —— Lambda

前序

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

初识lambda

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

mName.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
    }
});

image

但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实例")
}
image

      当单个方法接收多个函数式接口实例时,要么全部显式调用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)
}
image
  • 场景二:替代函数式接口实例
//替代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函数。

image

匿名函数

      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)
}

成员引用由类、双冒号、成员三个部分组成:

image

顶层函数和扩展函数都可以使用成员引用来表示:

//顶层函数
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")
image

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知识归纳(十四) —— 反射

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342