高阶函数

在说高阶函数之前我们先说一下如何在 Java 的方法中传递函数?Java 本身是不支持直接传递函数的,但是有一种间接的方式—接口,例如:

1
2
3
4
5
6
7
OnClickListener listener = new OnClickListener() {
@Override
void onClick(View v) {
// doSomething
}
};
view.setOnClickListener(listener);

其实这里只是想要一个 onClick 方法,但是限于语言的问题,这里不得不通过实现 OnClickListener 接口的方式来传递 onClick 方法。

Kotlin 支持传递函数类型,需要说明的是函数类型不是一种类型而是一类类型。对于函数类型,需要指明参数类型和返回值类型,例如:

1
2
3
4
5
6
7
8
fun test(hello: (String) -> Unit) {
// nop
}

// (String) -> Unit 对应的函数
fun hello(content: String) {
// nop
}

通过对函数类型的了解,可以引入高阶函数的概念:

高阶函数:参数或者返回值为函数类型的函数,没有什么特别的

函数类型既然可以作为参数,当然也可以作为变量,但是需要使用 ::,例如:

1
2
3
4
5
6
7
8
9
fun hello(content: String) {
// nop
}

// 作为函数变量
val temp = ::hello

// 作为函数参数
test(::hello)

那么这个 :: 到底是什么呢?官方说法,这个 :: 叫做函数引用 Function Reference。但是为什么不直接写函数名,而是要加上这个前缀呢?

因为加了 :: ,这个函数才成了对象。对于上面的内容可以反编译成字节码再编译成 Java 代码,就可以很清楚的知道 :: 的作用了。

1
2
3
4
5
6
7
class Func {
fun hello(content: String) {
// nop
}

val temp = ::hello
}
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
public final class Func {
@NotNull
private final KFunction temp = new Function1((Func)this) {
// $FF: synthetic method
// $FF: bridge method
public Object invoke(Object var1) {
this.invoke((String)var1);
return Unit.INSTANCE;
}

public final void invoke(@NotNull String p1) {
Intrinsics.checkNotNullParameter(p1, "p1");
((Func)this.receiver).hello(p1);
}
};

public final void hello(@NotNull String content) {
Intrinsics.checkNotNullParameter(content, "content");
}

@NotNull
public final KFunction getTemp() {
return this.temp;
}
}

Kotlin 里面 [函数可以作为参数] 的本质,是因为 Kotlin 中的函数可以作为对象存在。因为只有对象才可以作为参数传递,也只有对象才能赋值给变量。但是 Kotlin 中的函数本身的性质又决定了它没办法当做成一个对象。怎么办?Kotlin
的选择是创建一个和函数具有相同功能的对象,创建方式就是使用 ::

匿名函数

给函数类型变量赋值,除了使用 :: 拿现成的函数使用,还可以这么做:

1
2
3
val test = fun hello(content: String) {
// nop
}

另外,这种写法的话,函数的名字其实就没用了,所以你可以把它省掉:

1
2
3
val test = fun(content: String) {
// nop
}

这种写法叫做匿名函数。为什么叫匿名函数?很简单,因为没有名字啊。另外呢,刚才那种左边右边都有名字的写法,Kotlin 是不允许的。右边的函数既然要名字也没有用,Kotlin 干脆就不许它有名字了。所以开篇的 Java 代码在 Kotlin 中可以这么写:

1
2
3
4
5
6
fun setOnClickListener(onClick: (View) -> Unit) {
this.onClick = onClick
}
view.setOnClickListener(fun(v: View): Unit) {
// dosomething
})

注意:匿名函数不是一个函数,它是一个函数类型的对象

Lambda

1
2
3
view.setOnClickListener(fun(v: View): Unit) {
// dosomething
})

如果 Lambda 是函数中的最后一个参数,那么可以把 Lambda 写在函数括号的外面,如下:

1
2
3
view.setOnClickListener() { v: View -> 
// dosomething
})

如果 Lambda 是函数中唯一的参数,还可以把函数括号去掉,如下:

1
2
3
view.setOnClickListener { v: View -> 
// dosomething
})

如果 Lambda 是单参数的,还可以把 参数去掉,如下:

1
2
3
view.setOnClickListener {
// dosomething
})

注意:

  • Lambda 和匿名函数很像,都是函数类型的对象。
  • Lambda 的返回值是取函数最后一行的值,使用 return 会作为外层函数的返回值直接结束外层函数。

对比 Java 的 Lambda

Java 从 8 开始引入了对 Lambda 的支持,对于单抽象方法的接口——简称 SAM 接口,Single Abstract Method 接口——对于这类接口,Java 8 允许你用 Lambda 表达式来创建匿名类对象,但它本质上还是在创建一个匿名类对象,只是一种简化写法而已,所以 Java 的 Lambda 只靠代码自动补全就基本上能写了。而 Kotlin 里的 Lambda 和 Java 本质上就是不同的,因为 Kotlin 的 Lambda 是实实在在的函数类型的对象,功能更强,写法更多更灵活。

Kotlin 在 1.4 之前是不支持使用 Lambda 的方式来简写匿名类对象的,但是当和 Java 交互的时候,Kotlin 支持这种用法:当你的函数参数是 Java 的单抽象方法的接口的时候,你依然可以使用 Lambda 来写参数。但这其实也不是 Kotlin 增加了功能,而是对于来自 Java 的单抽象方法的接口,Kotlin 会为它们额外创建一个把参数替换为函数类型的桥接方法,让你可以间接地创建 Java 的匿名类对象。

这就是为什么,你会发现当你在 Kotlin 里调用 View.java 这个类的 setOnClickListener() 的时候,可以传 Lambda 给它来创建 OnClickListener 对象,但你照着同样的写法写一个 Kotlin 的接口,你却不能传 Lambda。

1
2
3
4
5
6
7
8
9
10
11
12
interface Listener {
fun onAction()
}

fun setOnClickListener(listener: Listener) {
// nop
}

//下面的写法是错误的
setOnClickListener {
// dosomething
}

但是在 Kotlin 1.4 之后添加了一个新的特性:函数式接口,具体可以看这里官网。使用 fun interface 后,上面的代码就可以正常运行:

1
2
3
4
5
6
7
8
9
10
11
12
fun interface Listener {
fun onAction()
}

fun setOnClickListener(listener: Listener) {
// nop
}

//下面的写法是错误的
setOnClickListener {
// dosomething
}