kotlin lambda之 “带接受者的lambda”

lambda基础

lambda这一块是kotlin一大难点,作为初学者,本篇文章记录一下学习历程。主要讨论了labmda的基本语法,以及带有接受者的lambda这种特殊语法的含义。

基本语法

Lambda 表达式的完整语法形式如下:

val sum: (Int, Int) -> Int = { x: Int, y: Int -> x + y }

lambda 表达式总是括在花括号中, 完整语法形式的参数声明放在花括号内,并有可选的类型标注, 函数体跟在一个 -> 符号之后。如果推断出的该 lambda 的返回类型不是 Unit,那么该 lambda 主体中的最后一个(或可能是单个) 表达式会视为返回值。

lambda的语法糖

//编译器类型推断,简化类型声明
val sum = { x: Int, y: Int -> x + y }

//当lambda表达式作为函数的末尾参数时
//可以放在圆括号之外
val product = items.fold(1) { acc, e -> acc * e }

//如果lambda是函数的唯一参数
//可以省略圆括号
run { println("...") }

//当lambda只有一个参数,且参数类型可以被推断出
//会自动生成一个it代替参数
val ss = "hello".filter { it > 's' } 
val ss = "hello".filter { param: Char -> param > 's'}

//lambda默认返回最后一行表达式的value
//也可以用限定符显式返回
val ss = "hello".filter {
    val filterMethod = it > 's'
    filterMethod
}
val ss = "hello".filter {
    val filterMethod = it > 's'
    return@filter filterMethod
}

注意,labmda是表达式的语法,很多情况下与函数结合使用,由于函数也存在一些语法糖,所以容易把二者混为一谈

//当函数只有一个表达式时,可以省略大括号,以‘=’连接
//这个特性和lambda没有关系
fun test(block: ()->Unit) = block() 

带有接受者的lambda

在考虑这个问题前,从简单的lambda开始分析
说明:示例代码中的函数和类,如没有改动,就复用上一个代码块的内容

block : (A) -> Unit

简单的lambda中,参数一般是8种基本类型。稍微复杂的lambda,需要传递自定义类型的对象。我们可以写成

class A {
    fun doSomething() {
        print("A do something\n")
    }
}

fun main() {   
    //这个lambda变量,接收一个A类型的参数,并执行一些操作
    val block: (A) -> Unit = { objectA: A ->
        objectA.doSomething()
    }
    //根据上述语法糖,简写为
    val block: (A) -> Unit = {
        it.doSomething()    
    }
}
  • 如果将带有自定义对象参数的lambda,作为另一个函数的参数,可以写成
fun demoOne(block: (A) -> Unit) {
    print("demoOne Done\n")
}

fun main() {
    val block: (A) -> Unit = {
        it.doSomething()    
    }
    demoOne(block)
}

在这里插入图片描述
这很合理,因为demoOne()里没有任何调用block的代码。

  • 那么如何使用这种lambda呢,可以写成
fun demoOne(block: (A) -> Unit) {
    val a = A()
    block(a)
    print("demoOne Done\n")
}

fun main() {
    //这里用语法糖简化
    demoOne {
        it.doSomething()
    }
}

在这里插入图片描述
很简单,这个lambda需要一个A类型的参数,所以new一个送进去即可

  • 事实上,上述使用方法,还有几种类似的写法
fun demoOne(block: (A) -> Unit) {
    block.invoke(A())
    print("demoOne Done\n")
}

fun demoOne(block: (A) -> Unit) {
    A().apply(block)
    print("demoOne Done\n")
}

fun demoOne(block: (A) -> Unit) {
    A().apply {
        block(this)    
    }
    print("demoOne Done\n")
}

在这里插入图片描述
运行结果都相同。至于原理,请见下文

  • 如果我不仅想调用A的方法,还想调A的属性,可以写成
class A {
    val aa = "NAME A\n"
    
    fun doSomething() {
        print("A do something\n")
    }
}

fun demoOne(block: (A) -> Unit) {
    block(A())
    print("demoOne Done\n")
}

fun main() {
    demoOne {
        print(it.aa)
        it.doSomething()
    }
}

在这里插入图片描述
这也很合理,因为传入demoOne的这个lambda里,it是一个匿名的A的对象,访问自己的属性当然能访问到。注意这里的aa是公有的,如果写成private,就需要暴露一个公有方法来间接访问。

  • 上面都是针对A的属性和方法,那能不能在传入的lambda中操作其他类的属性和方法。可以写成
class A {
    val aa = "NAME A\n"
    
    fun doSomething() {
        print("A do something\n")
    }
}

class B {
    val bb = "NAME B\n"
    
    fun doSomething() {
        print("B do something\n")
    }
}

fun demoOne(block: (A) -> Unit) {
    block(A())
    print("demoOne Done\n")
}

fun main() {
    var b = B()
    demoOne {
        print(b.bb)
        b.doSomething()
        print(it.aa)
        it.doSomething()
    }
}

在这里插入图片描述
可以发现在传入lambda时,可以操作其他类。原理见下文。

  • 总结一下,对于block: (A) -> Unit这种lambda的基本使用。完整demo代码
class A {
    val aa = "NAME A\n"
    
    fun doSomething() {
        print("A do something\n")
    }
}

class B {
    val bb = "NAME B\n"
    
    fun doSomething() {
        print("B do something\n")
    }
}

fun demoOne(block: (A) -> Unit) {
    block(A())
    print("demoOne Done\n")
}

fun main() {
    var b = B()
    demoOne {
        print(b.bb)
        b.doSomething()
        print(it.aa)
        it.doSomething()
    }
}

block: A.() -> Unit

下面可以开始学习“带有接收者的lambda”这个概念
官方文档对应部分 http://www.kotlincn.net/docs/reference/lambdas.html

  • 从最基本的调用开始
class A {
    val aa = "NAME A\n"
    
    fun doSomething() {
        print("A do something\n")
    }
}

fun demoTwo(block: A.() -> Unit) {
    print("demoOne Done\n")
}

fun main() {   
    //这里仿照上一个例子,写一个lambda
    **val block: A.() -> Unit = { Afunc: A.() ->
        Afunc()
    }
    
    val block: A.() -> Unit = { Afunc: A ->
        Afunc()
    }**
    //正确的写法
    val block: A.() -> Unit = {
        //to do
    }
    demoTwo(block)
}

上述加粗代码是编不过的,编译器会提示Expected no parameters,也就是说这种lambda不接收参数?
正确代码运行结果如下。很容易理解,传递了一个空的lambda自然什么都不会做
在这里插入图片描述

  • 仿照block: (A) -> Unit的写法,如何调用到A的方法呢
class A {
    val aa = "NAME A\n"
    
    fun doSomething() {
        print("A do something\n")
    }
}

fun demoTwo(block: A.() -> Unit) {
    block(A())
    block.invoke(A())
    A().block()
    A().apply(block)
    A().apply {
        block(this)
    }
    A().apply {
        this.block()
    }
    print("demoOne Done\n")
}

fun main() {   
    demoTwo {
        doSomething()
        //还可以写成。注意这一处变化
        //this.doSomething()
    }
}

在这里插入图片描述
上述6种写法都能run,而block: (A) -> Unit有下面4种写法。

fun demoOne(block: (A) -> Unit) {
    block(A())
    block.invoke(A())
    A().apply(block)
    A().apply {
        block(this)
    }
    print("demoOne Done\n")
}

首先考虑二者共有的四种调用方法。对于block: (A) -> Unit来说,其接收一个A类型的对象,有了A的实例,这四种调用都是显而易见成立的。但对于block: A.() -> Unit,为什么也能run呢,原因放到原理部分分析。
然后考虑新增的A().block()和A().apply{this.block()}。直观上看起来,似乎block是A的一个成员方法,但显然,block是一个独立的lambda。那为啥A()能直接调block呢,是不是有点像扩展方法。具体和扩展有何异同,在原理部分分析。

  • 如何调用A的属性呢,很容易写出
fun main() {   
    demoTwo {
        //同样注意,这里我们是直接访问的,说明this指针指向A
        print(aa)
        doSomething()
    }
}

在这里插入图片描述

  • 再考虑操作类的属性和方法
class B {
    val bb = "NAME B\n"
    
    fun doSomething() {
        print("B do something\n")
    }
}

fun main() {   
    var b = B()
    demoTwo {
        print(aa)
        doSomething()
        print(b.bb)
        b.doSomething()
    }
}

在这里插入图片描述

  • 看起来和block : (A)-> Unit的操作没区别。考虑下面这个特殊情况
fun main() {   
    fun doSomething() {
        print("main do something\n")
    }
    
    var b = B()
    demoTwo {
        print(aa)
        doSomething()
        this.doSomething()
        print(b.bb)
        b.doSomething()
    }
}

在这里插入图片描述
根据上述的分析,demoTwo传入的lambda中,this指针默认指向A。但是这里所说的默认,其优先级仍然是低于局部变量的,如有同名的方法和属性,优先走局部变量。如果需要明确制定A,只需要显式使用this指针即可。

  • 总结一下,block: A.() -> Unit 的基本使用demo代码
class A {
    val aa = "NAME A\n"
    
    fun doSomething() {
        print("A do something\n")
    }
}

class B {
    val bb = "NAME B\n"
    
    fun doSomething() {
        print("B do something\n")
    }
}

fun demoTwo(block: A.() -> Unit) {
    block(A())
    print("demoOne Done\n")
}

fun main() {   
    fun doSomething() {
        print("main do something\n")
    }
    
    var b = B()
    demoTwo {
        print(aa)
        doSomething()
        this.doSomething()
        print(b.bb)
        b.doSomething()
    }
}

原理探索

虽然基本用法可以通过试错搞出来,但不懂原理遇到bug就gg了。所以来尝试瞟一眼原理
先写一个简单的调用对比

fun main() {
    demoOne { 
        it.doSomething()
    }
    demoTwo { 
        doSomething()
        this.doSomething()
    }
}

fun demoOne(block: (A) -> Unit) {
    block.invoke(A())
    print("1\n")
}

fun demoTwo(block: A.() -> Unit) {
    block.invoke(A())
    print("2\n")
}

class A()
{
    fun doSomething() {
        print("A do something \n")
    }
}

直接反编译,整理一下代码

public final class DemoKt {
   public static final void main() {
      demoOne((Function1)null.INSTANCE);
      demoTwo((Function1)null.INSTANCE);
   }

   // $FF: synthetic method
   public static void main(String[] var0) {
      main();
   }

   public static final void demoOne(@NotNull Function1 block) {
      Intrinsics.checkParameterIsNotNull(block, "block");
      block.invoke(new A());
      String var1 = "1\n";
      boolean var2 = false;
      System.out.print(var1);
   }

   public static final void demoTwo(@NotNull Function1 block) {
      Intrinsics.checkParameterIsNotNull(block, "block");
      block.invoke(new A());
      String var1 = "2\n";
      boolean var2 = false;
      System.out.print(var1);
   }
}
// A.java
public final class A {
   public final void doSomething() {
      String var1 = "A do something \n";
      boolean var2 = false;
      System.out.print(var1);
   }
}

发现了神奇的事情,demoOne和demoTwo传入的参数,都是Function1类型的。众所周知,kt中的函数是一等公民,其类型由Function0、Function1、Function2等等一直到Function22。其数字代表函数的参数个数。但是我们在前面学习demoTwo的调用时,下面的写法是报错的,提示Expected no parameters。

val block: A.() -> Unit = { Afunc: A.() ->
        Afunc()
    }
    
val block: A.() -> Unit = { Afunc: A ->
        Afunc()
    }

如果想继续深究为什么,需要考察A.()这个语法机制的设计原理。我们不必理解这么深,只要注意到上述反编译代码中,demoOne和demoTwo的方法体是完全一样的。特别是block.invoke(new A()) 这一条真正的调用指令。结合官方文档中这句话
在这样的函数字面值内部,传给调用的接收者对象成为隐式的this,以便访问接收者对象的成员而无需任何额外的限定符,亦可使用 this 表达式 访问接收者对象
我们可以得出初步结论,所谓带接受者的lambda就是把接受者对象作为隐式的this指针传入,替代了原本作用域的this。
举个例子

class test {
    var name = "test\n"
    init {
        var name = "init\n"
        demoOne {
           print(name) //局部变量最高优先级,所以是init
           print(it.name) //it匿名A对象,所以是aa
           print(this.name) //this指向当前作用域的对象,所以是test
        }
        demoTwo {
           print(name)//局部变量最高优先级,所以是init
           print(this.name) //A.()语法隐式改写了this指针,所以是aa
        }
    }
}

class A()
{
    var name = "aa\n"
    fun doSomething() {
        print("A do something \n")
    }
}

fun demoOne(block: (A) -> Unit) {
    block.invoke(A())
}

fun demoTwo(block: A.() -> Unit) {
    block.invoke(A())
}

在这里插入图片描述

  • 还剩下最后一个问题,A.() -> Unit这种lambda语法和扩展有什么异同?
class A {
    var aa = "aa"
}

fun A.printA() {
    print(aa)
}

class B {
    var a = A()
    init {
        a.printA()
    }
}

反编译

public final class B {
   @NotNull
   private A a = new A();

   @NotNull
   public final A getA() {
      return this.a;
   }

   public final void setA(@NotNull A var1) {
      Intrinsics.checkParameterIsNotNull(var1, "<set-?>");
      this.a = var1;
   }

   public B() {
      DemoKt.printA(this.a);
   }
}

// DemoKt.java
public final class DemoKt {
   public static final void printA(@NotNull A $this$printA) {
      Intrinsics.checkParameterIsNotNull($this$printA, "$this$printA");
      String var1 = $this$printA.getAa();
      boolean var2 = false;
      System.out.print(var1);
   }
}
// A.java
public final class A {
   @NotNull
   private String aa = "aa";

   @NotNull
   public final String getAa() {
      return this.aa;
   }

   public final void setAa(@NotNull String var1) {
      Intrinsics.checkParameterIsNotNull(var1, "<set-?>");
      this.aa = var1;
   }
}

在B的init里调用printA方法时,实际调用了DemoKT的方法,因为扩展并不是真正改写了A类。而DemoKt::printA()接受的是A类型的对象,真正的方法体也是写在DemoKt::printA()里。
对比lambda,主要的区别是传递给DemoKt的参数不一样,一个是A的对象,一个是Function对象
应用

  • 带有接受者的lambda被推荐用于DSL的实现,但不会用DSL,所以这块暂且跳过
  • 根据官方文档的示例,发现这种lambda语法或许可以用于设计工厂方法,代码如下。这种写法没啥性能优势,感觉唯一的好处是增强了A的抽象能力和复用能力。A里提供了一堆基础属性和方法,消费者可以随意传入一个method,对这些属性和方法进行改写和调用。
class A {
    companion object {
        fun createA(method: A.() -> Unit): A {
                //相当于执行者
                var a = A()
                a.method()
                return a
            }
    }
    //这里只是定义一个变量,可以搞得更复杂一些
    var aa = "none"  
}

fun main() {
    //通过lambda定义不同的method
    //来生产不同的产品
    val firstA = A.createA {
        //相当于设计稿
        this.aa = "first"
    }
    val secondA = A.createA {
        this.aa = "second"
    }
}

结语

还有很多点没有学习到,比如A.(B) -> C 可以与 (A,B) -> C相互转换,以及如下代码的区别。等等…

fun a(block:()->Unit) {block.invoke()}
fun a(block:()->Unit) = {block.invoke()} //这种写法下block是无法执行的
  • 7
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值