Kotlin 真香之 Lambda 表达式

| PV()

函数类型与函数实例化

在kotlin中,函数与数据同样是一等公民,这意味着它们可以存储在变量与数据结构中、作为参数传递给其他高阶函数以及从其他高阶函数返回,可以像操作任何其他非函数值一样操作函数。

1
2
val square = { item: Int -> item * item }
print(square(3))

那么如果函数与数据同样是一等公民,那么数据拥有的类型实例,函数是不是也同样拥有呢?答案是肯定的。

函数类型

函数也是拥有类型的,不过不同于IntString这些大家耳熟能详的,而是以(Type1,Type2,...)->Type这种形式来定义的。例如上文中提到的square,它的类型就是(Int)->Int

1
2
3
4
val square: (Int) -> Int = { item: Int ->
item * item
}
print(square(3))

我们知道数据类型是可以继承的,以满足我们个性化的定制需求,函数类型也可以吗?当然!

1
2
3
4
5
class Square : (Int) -> Int {
override fun invoke(p1: Int): Int {
return p1 * p1
}
}

函数类型也可以定义在函数签名之中,我们看看这样的例子,参数action的类型就是函数类型:

1
2
3
4
5
6
7
8
fun doWithInt(item: Int, action: (Int) -> Int): Int {
return action(item)
}

fun main() {
val square: (Int) -> Int = { item: Int -> item * item }
println(doWithInt(3, square))
}

函数实例

那么函数有了类型,就一定有实例,就像Int可以有3,String可以有HelloWorld。我们来看看函数实例有什么样的要求。

首先已经声明的函数,都是具体的实例,无论是在class内部,还是属性等等。来看看例子,其中::表示创建对方法或者class的引用。

1
2
3
4
5
6
7
fun main() {
print(doWithInt(16, ::square))
}

fun square(item: Int): Int {
return item * item
}

除了前面的方式以为,还有另两种常用的实例化方法。一是lambda表达式、另一个是匿名函数。

先看看匿名函数的方法,匿名函数与普通函数的申明,区别仅仅在有没有方法名字上:

1
2
3
4
val square = fun(item: Int): Int {
return item * item
}
print(doWithInt(16,square))

另一个就是本文的重点lambda表达式,其语法也很简单:

{ param1 : Type1, param2 : Type2 , … -> body }

1.永远在大括号内。
2.参数两边不能有小括号。如果没有参数,可以留白,同时->也可以去掉。
3.方法体如果有多行,直接换行即可。

1
2
3
4
val square = { item:Int ->
print("HelloWorld")
item*item
}

Lambda表达式想对于匿名函数提供了更多便利的地方,大家尽量使用lambda表达式。

1.it代替单个参数
在很多情况下,参数可能只有一个,在lambda表达式中就可以用it来代替,从而简化语法。

1
2
3
4
5
6
val square1:(Int)->Int = {
item: Int -> item * item
}
val square:(Int)->Int = {
it * it
}

2.lambda表达式作为最后一个参数

在Kotlin中有一个约定:如果函数的最后一个参数是函数,那么作为相应参数传入的lambda表达式可以放在圆括号之外,这种语法也叫做拖尾lambda表达式。

下面两种doWithInt的调用是等价的。

1
2
3
4
5
6
7
8
fun doWithInt(item: Int, action: (Int) -> Int): Int {
return action(item)
}

doWithInt(25, { it * it })
doWithInt(25) {
it * it
}

SAM转换

前面我们讨论kotlin中的lambda很是欢畅,但到了Java这一侧就不那么愉快了。毕竟Java8中的lambda表达式只是闭包和invokeDynamic拼凑起来的四不像,Java8并没有函数这种类型,更别说kotlin对接的Java平台还只是Java7。

SAM = Single Abstract Method,即唯一抽象方法!

但兽人永不为奴,啊,呸,是kotlin无敌,针对SAM给我们提供了一个很甜的语法糖。我们来看看下面这个例子:

1
2
3
var listener = View.OnClickListener {
it.visibility = View.GONE
}

详细分析一下,View.OnClickListener的官方函数是java的,申明如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* Interface definition for a callback to be invoked when a view is clicked.
*/
public interface OnClickListener {
/**
* Called when a view has been clicked.
*
* @param v The view that was clicked.
*/
void onClick(View v);
}

View.OnClickListener listener = newView.OnClickListener() {
@Override
public void onClick(Viewv) {
v.setVisibility(View.GONE);
}
};

Java这端需要的是OnClickListener匿名类的实例,而前面代码中提供的还只是lambda表达式。两者是不同的东西呀,那是怎么回事呢?这里需要提出一个SAM构造器的概念。

FunctionInterfaceName { lambda_function }

这是专门为Java提供的语法糖,用于将lambda表达式转换成相应的匿名类的实例。

同样,还可以在函数调用中使用SAM转换,由于类型是可以推断出来的,前面的FunctionInterfaceName甚至可以省略,下面两种写法是等价的。

1
2
3
4
5
6
7
8
9
10
11
fun doWithView(view: View) {
view.setOnClickListener(View.OnClickListener {
it.visibility = View.GONE
})
}

fun doWithView(view: View) {
view.setOnClickListener {
it.visibility = View.GONE
}
}

我们扩展一下,如果是kotlin会怎样呢?首先我们申明一个kotlin的接口。

1
2
3
interface KotlinInterface {
fun doSth()
}

接着我们和前面Java一样,试图通过SAM构造器的方式,来创建匿名内部类的实例,情况会怎样?

1
2
// this code will leads to compile error.
// var listener = KotlinInterface{ print("HelloWorld") }

这里居然报错了!!!

kotlin-sam-failed

聪明的读者已经想到了,Kotlin中如果使用这种语法,是在调用KotlinInterface的构造函数,由于这是一个接口,并没有对应的构造函数,所以就报错了。那这里就奇怪了,Java中是SAM构造器语法,到Kotlin中这就是调用构造函数?答案确实是这样的,这是专门为Java提供的语法糖!

所以问题的关键是,为什么Kotlin要做这个限制呢?原因在于kotlin拥有更高阶的能力—高阶函数。对于Java而言,由于没有函数这个类型,对于SAM,只能是构造一个匿名对象,变相地绕过语言层面的限制。但kotlin不一样,其函数本身就是一等公民,没必要舍近求远。针对OnClickListener,Kotlin更愿意这样来申明:

1
2
3
class View {
var onClicked:((view:View)->Unit)? = null
}

带Receiver的lambda

结合前面的文章,我们知道定义扩展函数,这个语法同样适用于lambda表达式。那么既然有了扩展函数,为什么还要给lambda也提供同样的能力呢?

假设,我们构建一个 json 的DSL (Domain Specific Language),首先定义了一个生成键值对的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class JsonBuilder {
fun warpKeyValue(tag: String, handler: (StringBuilder) -> Unit): String {
valbuilder = StringBuilder()
builder.append("\"$tag\":")
handler(builder)
return builder.toString()
}

}

fun main() {
print(JsonBuilder().warpKeyValue("key") {
it.append(1)
})
}
// will output:
// "key":1

这里关键在于it,对于DSL而言,如果每个地方都需要调用it的话,会很别扭。前面提到扩展函数,可以用this,或者省略,来调用Receiver的方法。如果我们给Receiver扩展一个lambda表达式,那就可以省略it了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class JsonBuilder {

fun warpKeyValue(tag: String, handler: StringBuilder.() -> Unit): String {
valbuilder = StringBuilder()
builder.append("\"$tag\":")
builder.handler()
return builder.toString()
}

}

fun main() {
print(JsonBuilder().warpKeyValue("key") {
append(1)
})
}
// will output:
// "key":1

那么为什么我们非得省略it这个了?是为了DSL更加美观,让使用者不必明白kotlin的相关语法细节。

我们来实现一个极简版本的jsonDSL(-先不考虑JSON数组的情况-),看看会变成什么样子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import java.lang.StringBuilder

class JsonBuilder {

private val builder = StringBuilder()

fun key(key: String, action: JsonBuilder.() -> Unit): JsonBuilder {
builder.append("{")
builder.append("\"$key\":")
valvalueBuilder = JsonBuilder()
valueBuilder.action()
builder.append(valueBuilder.build())
builder.append("}")
return this
}

fun value(item: Any) {
when (item) {
isString -> builder.append("\"$item\"")
isInt, Float, Double, Long -> builder.append("$item")
isJsonBuilder -> builder.append(item.build())
else -> throw IllegalArgumentException("unsupported type")
}
}

fun build(): String {
return builder.toString()
}
}

fun main() {
val builder = JsonBuilder()
builder.key("Family") {
key("Parent") {
key("Son") {
value("Tom")
}
}
}
print(builder.build())
}

最后会输出

kotlin-sam-failed

如果每次调用key,value方法都需要加上it,就达不到DSL的目的了。

这里给大家一个GitHub上,大神写的kotlinDSL的Demo,GitHub-rybalkinsd/kohttp:Kotlin DSL http client,语法还是相当舒服的。


文档信息