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是无法执行的