lambda 与成员引用

总述

  1. lambda 编译后生成的类都继承 Lambda 类,并根据参数个数实现 FunctionN 接口 —— N 表示参数个数,所以 lambda 的实际类型是 FunctionN

  2. 属性成员引用的实际类型是 PropertyReference1 子类,而后者实现了 KProperty1 ,KProperty1 定义如下:

    public interface KProperty1<T, out R> : KProperty<R>, (T) -> R
    

由于 PropertyRefrence1 外界无法使用,所以可以认为 属性成员引用的实质类型是 KProperty1

因此,属性成员可直接赋值给初始值为 lambda 表达式的变量,但反之不行

属性初始引用与 lambda
  1. 方法成员引用的实际类型是 KFunctionN,但编译后生成的类实现了函数类型接口,因此方法成员引用可直接赋值给初始值为 lambda 的变量,但反之不行
方法成员引用与 lambda
  1. lambda 是函数类型的实例,成员引用是函数类型的子类的实例。因此成员引用可直接赋值给 lambda,但反之不行

  2. 函数类型变量的运行其实质就是运行其内部的 invoke 方法 —— 参考 invoke 约定。


lambda

lambda 表达式的本质是 可以传递给其他函数的小段代码

lambda 是函数类型的实例,因此可以直接赋值给函数类型的变量。

每一个 lambda 都会被编译成一个匿名内部类,传入 lambda 的地方就是传入一个该类的实例。

fun test(id:String){
    Thread{ // 使用 lambda
        println("runnable")
    }.start()
}
// 创建一个内部类
class Test$1 : Runnable{
    override fun run() {
        println("runnable")
    }
}

fun test(id:String){
    Thread(Test$1()).start()// 实际上是传入了一个内部类的实例
}

语法

  1. 始终用花括号包围。参数列表不需要用括号括起来。使用箭头把参数列表与函数体分开

如图:

语法
  1. lambda 表达式中可以含有多个表达式,只有最后表达式的结果是 lambda 的返回值。

使用

  1. 可以直接赋值给一个变量,然后将这个变量当作普通函数;或者直接运行表达式

    fun main(args: Array<String>) {
        val l = {x:Int,y:Int -> x+y}
        // 此处要加分号。不然会认为下个表达式是 println() 的一个参数
        println(l(2,3));
        { println("xx")}()
    }
    
  2. 如果表达式是最后一个参数,可以将表达式放到括号外面;如果只有表达式一个参数,可以省略小括号。如果要传递两个或更多的表达式,不能将表达式定义在小括号外面。

    fun main(args: Array<String>) {
        val list = listOf(Person(1), Person(3), Person(2))
        // 正常传递表达式
        println(list.maxBy({ p: Person -> p.age }))
        // 将表达式提取到括号外面
        println(list.maxBy() {p: Person -> p.age  })
        // 只有一个参数,省略小括号
        println(list.maxBy { p: Person -> p.age })
    }
    
  3. 如果能推导出参数类型,则可以省略表达式中的参数类型;如果只有一个参数,且类型可以推导出类型,可以使用 it 指代参数,连参数名都不用写,直接使用 it。接上例,可以再简化:

        // 能推导出类型,则省略参数类型
        println(list.maxBy { person -> person.age })
        // 能推导出类型,且只有一个参数,则可以省略参数名使用 it 指代
        println(list.maxBy { it.age })
    
  4. 如果不需要参数,可以省略 -> ,直接写表达式的执行语句:

    fun main(args: Array<String>) {
        var a = 10
        val f = { println("$a") }
    }
    

访问变量

与对象表达式一样,表达式可以访问这个函数的参数,以及定义在表达式之前的局部变量

kt 中允许表达式访问非 final 变量(通过 var 声明的变量),并修改这些变量。

fun test(name: String){
    var age = 10
    // 可以访问参数 name,也可以访问定义在表达式前的 age ,但不能访问 sex
    { println("${name}  ${age}")}()

    var sex = "f"
}

实现原理

  1. 当表达式捕获 final 变量时,变量的值会被复制下来,和使用这个值的表达式一起存储。java 中也是这个原理。

  2. 对于非 final 变量,它的值会被封装在一个特殊的包装器中,将包装器的引用和表达式一起存储。在整个过程中,引用不会发生变化,但包装器中的属性可以改变

    // 包装器
    class Ref<T>(var value:T)
    
    fun test(name: String){
        val age = 10
        val ref = Ref(age)
        val lambda = {ref.value++}
    }
    

    与表达式一起存储的是 ref,ref 在整个过程中都不会发生变化,但其属性 value 却可以改变。


成员引用

它提供了一个简明语法,来创建一个调用单个方法或者访问单个属性的函数。

成员引用在编译后会生成一个内部类的实例(该内部类有 invoke 方法),执行成员引用就相当于执行 invoke 方法,而 invoke 方法内部会调用引用的函数(函数成员引用时)或者访问指定的属性(属性成员引用时)。

相较于普通的直接调用方法和引用属性,成员引用是一个函数类型的实例,因此可以将成员引用作为实参传递给函数类型的形参:

fun main(args: Array<String>) {
    val t = Obj::a
    println(t is (Obj) -> Int) // true

    test(t) // 11
}

fun test(a: (Obj) -> Int) {
    val obj = Obj(11)
    println(a(obj)// a() 相当于执行成员引用,所以会获取到对应的属性值
}

class Obj(val a: Int)

其格式如下:

成员引用
  1. 无论 member 是函数还是属性,其后都不加 ()

  2. 如果是顶层函数、属性,则省略 Class ,直接以 :: 开头。

  3. 如果引用的是构造函数,则写成 ::类名

  4. 成员引用一样适用于扩展函数、属性。

使用

成员引用会创建一个函数,执行该函数时,会执行指定的方法或访问指定的属性。如下例,main 函数中的 get 与 age 都是函数,执行 get 时会执行 person.get() ,而执行 age 时会获取 person 中的 age 属性的值。

  1. kt 1.0 中,执行成员引用时,始终需要提供一个该类的实例。

  2. kt 1.1 中,可以直接使用使用对象进行定义,如下例中的 age1 与 get1 的定义方式。在执行时,不需要再传入实例对象。

fun main(args: Array<String>) {
    val person = Person(1111)

    val age = Person::age
    println(age(person))
    val get = Person::get
    println(get(person))

    val age1 = person::age
    println(age1())
    val get1 = person::get
    println(get1())
}

data class Person(val age: Int) {
    fun get() = age * 10
}

与 lambda 比较

成员引用与 lambda 是互通的,可以互相使用

无论是成员引用还是 lambda ,其实现时都会转成 Function 的实例。因此,它们是同一类型,可以相互赋值 —— 只要泛型一致即可。如下,所有的输出都是 true:

fun main(args: Array<String>) {
    val t = Obj::a
    val l = { a: Int, b: Int -> a + b }
    println(t is Function<*>)
    println(l is Function2<*, *, *>)
    val f = Obj::test
    println(f is Function<*>)
    println(f is Function1<*, *>)
}

class Obj(val a: Int) {
    fun test() {}
}

如定义一个 test() 函数接收一个表达式。可以直接传入一个表达式,也可以传入一个成员引用。

fun main(args: Array<String>) {
    val person = Person(1111)

    test(person) { it.get() }
    test(person,Person::get)
}

data class Person(val age: Int) {
    fun get() = age * 10
}

// 该方法接收一个 lambda 表达式
fun test(p: Person, a: (Person) -> Int) = println(a(p))

可以发现上述的表达式很简单,直接调用了 Person 的 get 方法。这种情况下,使用成员引用更方便。

当需要定义的表达式的功能已经被别的方法实现过,但使用表达式的函数只接收表达式时,可以使用成员引用。如上例中的 test 方法,它接收表达式,而表达式要实现的功能是 Person#get() 方法已经实现过的,所以直接使用成员引用即可。


lambda 管理资源

常见模式是:先获取一个资源,完成一个操作后,关闭该资源。一般在 try 中获取资源,将操作封装成 lambda 表达式,然后在 finally 中关闭资源。

如下:首先自动加锁,然后执行操作,操作执行完成后释放锁。这样封装了加锁、释放锁的逻辑,调用者只关注自己要完成的操作。

fun <T> Lock.withLock(action: () -> T): T {
    lock()
    try {
        return action()
    } finally {
        unlock()
    }
}

带接收者的 lambda

lambda 表达式执行时,this 指向接收者对象。因此,凡是调用接收者中的方法,都可以直接调用

首先看一般情况下的 lambda 的定义:

```kotlin
fun main(args: Array<String>) {
    test{
        it.append("xxxx")
    }
}

fun test(b: (StringBuilder) -> Unit) {
    val sb = StringBuilder()
    b(sb)
    println(sb.toString())
}
```

调用 test 传入的表达式中,想要调用 sb 中的 append 方法,必须使用 it.append。使用带接收者的 lambda 后,如下:

fun main(args: Array<String>) {
    test{
        append("xxxx")
    }
}

fun test(b: StringBuilder.() -> Unit) {
    val sb = StringBuilder()
    b(sb)
    println(sb.toString())
 }

可以直接调用 append() ,而不需要使用 it.append()。

上例中,StringBuilder 是接收者类型,test() 中定义的 sb 就是接收者对象。

  1. 接收者可以当作参数传递到表达式中,也可以直接使用接收者点的表达式名的形式进行调用:

    fun buildStr(action: StringBuilder.() -> Unit) {
        val sb = StringBuilder()
        action(sb)
        sb.action()
        println(sb.toString())
    }
    
  2. 将函数参数中的一个参数类型移到括号外面,并使用 . 将它与其余参数区分开

定义方式
  1. 由扩展函数类似,当函数或 lambda 被调用时需要提供这个对象,它在函数体内是可用的。

lambda 转成类

如果一个 lambda 太复杂,可以将 lambda 转成实现了函数类型接口的的类,并重写其 invoke 方法

这种方法的优点时:从 lambda 体中抽取的函数的作用域尽可能的小,它仅在判断式内部可见:

fun main(args: Array<String>) {
    val test = arrayOf("a", "bb", "CC", "drewAA")
    for (a in test.filter(Test())) {
        println(a) // a CC
    }
}

class Test : (String) -> Boolean {
    override fun invoke(p1: String): Boolean = p1.startsWith("a") || isImportant(p1)

    private fun isImportant(s: String): Boolean = s.toUpperCase() === s
}

filter 要的是一个函数类型的实例,所以此处可以传入 lambda 表达式或者一个函数类型的子类 —— 本例中使用的是后者。

在 Test 的类中,invoke 会被 filter 调用,而 invoke 中又调用了 isImportant 方法 —— 这就是比 lambda 要方便的地方,可以在类中定义方法。


lambda 中的 return

只有调用 lambda 的函数是 inline 函数,才能在 lambda 中使用 return

lambda 中使用 return 时,会直接结束使用 lambda 的函数,而不是只结束 lambda 表达式。可以在表达式外层套一个 run 函数,return 时只返回 run 标签。如下述代码只会输出 run ,不会输出 test。因为第二次执行 test 时,调用了 return,会直接结束掉 main 方法。第一次只是结束掉 run 方法,所以还会执行下面的语句:

fun main(args: Array<String>) {

    run {
        test {
            return@run
        }
    }
    println("run")

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