1. 声明高阶函数
高阶函数就是以另一个函数作为参数或者返回值的函数。在kotlin中,函数可以用lambda或函数引用来表示。
例如:标准库中的filter函数将一个判断式函数作为参数,所以就是一个高阶函数
list.filter { x > 0 }
1.1 函数类型
为了声明一个以lambda作为实参的函数,需要知道如何声明对应的形参的类型。
先来看一个简单的例子:把lambda表达式保存在局部变量中。
// 有两个Int类型参数和Int类型的返回值的函数
val sum = {x: Int, y: Int -> x + y}
// 没有参数和返回值的函数
val action = { println("32") }
编译器会推导sum和action这两个变量具有函数类型。这些变量的显式类型声明是什么呢?
val sum: (Int, Int) -> Int = { x, y -> x + y }
val action: () -> Unit = { println("32") }
声明函数类型,需要将函数参数类型放在括号中,紧接着是一个箭头和函数返回类型。一个函数类型声明总是需要一个显式的返回类型,在这里Unit是不能省略的。
在lambda表达式{ x, y -> x + y }中是如何省略参数x,y的类型的呢?因为它们的类型已经在函数类型的变量声明中指定了,就不需要在lambda定义中重复声明。
函数类型的返回值也可以标记为可空类型:
var canReturnNull: (Int, Int) -> Int? = { null }
也可以定义一个函数类型的可空变量:
var funOrNull: ((Int, Int) -> Int)? = null
1.2 调用作为参数的函数
知道了怎么声明高阶函数,那如何去实现呢?
举一个例子:定一个简单的高阶函数,实现两个数字2和3的任意操作,然后打印结果
fun twoAndThree(operation: (Int, Int) -> Int) {
// 调用函数类型的参数
val result = operation(2, 3)
println("The result is $result")
}
twoAndThree { i, j -> i + j }
// The result is 5
twoAndThree { i, j -> i * j }
// The result is 6
调用作为参数的函数和调用普通函数的语法是一样的:把括号放在函数名后,并把参数放在括号中。
再举一个例子:实现一个简单版本的filter函数
其中filter函数是一个判断式作为参数。判断式的类型是一个函数,以字符作为参数并返回boolean类型的值。
fun String.filter(predicate: (Char) -> Boolean): String {
val sb = StringBuilder()
for (index in 0 until length) {
val element = get(index)
// 调用作为参数传递给"predicate"函数
if (predicate(element)) sb.append(element)
}
return sb.toString()
}
// 传递一个lambda,作为predicate参数
println("ab3d".filter { c -> c in 'a'..'z' })
// abd
1.3 在java中使用函数类
背后的原理是:函数类型被声明为普通的接口,一个函数类型的变量是FunctionN接口的一个实现。
在kotlin标准库定义了一系列的接口,这些接口对应于不同参数量的函数,Function0<R>(没有参数的函数),Function1<P1, R>(一个参数的函数)等。每个接口定义了一个invoke方法,调用这个方法就是执行函数。一个函数类型的变量就是实现了对应的FunctionN接口的实现类的实例,实现类的invoke方法包含了lambda函数体。
java8的lambda会被自动转换为函数类型的值
// kotlin 声明
fun processTheAnswer(f: (Int) -> Int) {
println(f(34))
}
// java
processTheAnswer.(number -> number + 1);
// 35
在旧版的java中,可以传递一个实现了接口函数中的invoke方法的匿名类的实例:
processTheAnswer(new Function1<Integer, Integer>() {
@Override
public Integer invoke(Integer integer) {
// 在java代码中使用函数类型(java8之前)
return integer + 1;
}
});
在java中使用kotlin标准库中以lambda作为参数的函数,必须显式的传递一个接受者对象作为第一参数:
List<String> list = new ArrayList<>();
list.add("23");
// 可以在java中使用kotlin标准库中的函数
CollectionsKt.forEach(list, s -> {
System.out.println(s);
// 必须显式的返回一个Unit类型的值
return Unit.INSTANCE;
});
1.4 函数类型的参数默认值和null值
举一个例子:使用了硬编码toString转换的joinToString函数
fun <T> Collection<T>.joinToString(
separator: String = ", ",
prefix: String = "",
postfix: String = ""
): String {
val result = StringBuilder(prefix)
for ((index, element) in this.withIndex()) {
if (index > 0) result.append(separator)
// 使用默认的toString方法将对象转换为字符串
result.append(element)
}
result.append(postfix)
return result.toString()
}
将集合中的元素转换为字符串,总是使用toString方法。可以定义一个函数类型的参数并用一个lambda作为它的默认值
fun <T> Collection<T>.joinToString(
separator: String = ", ",
prefix: String = "",
postfix: String = "",
// 声明一个以lambda为默认值的函数类型的参数
transform: (T) -> String = { it.toString() }
): String {
val result = StringBuilder(prefix)
for ((index, element) in this.withIndex()) {
if (index > 0) result.append(separator)
// 调用作为实参传递给 "transform" 形参的函数
result.append(transform(element))
}
result.append(postfix)
return result.toString()
}
val list = listOf("A", "B", "C")
// 传递一个lambda作为参数
println(list.joinToString { it.toLowerCase() })
// a, b, c
还可以声明一个参数为可空的函数类型。但是不能直接调用作为参数传递进来的函数,kotlin会因为检测到潜在的空指针异常而导致编译失败。我们可以显式地检查null
fun foo(callback: (() -> Unit)?) {
// ...
if (callback != null) {
callback()
}
}
可以利用函数类型是一个包含invoke方法的接口的具体实现。
举一个例子:使用函数类型的可空参数
fun <T> Collection<T>.joinToString(
separator: String = ", ",
prefix: String = "",
postfix: String = "",
// 声明一个函数类型的可空参数
transform: ((T) -> String)? = null
): String {
val result = StringBuilder(prefix)
for ((index, element) in this.withIndex()) {
if (index > 0) result.append(separator)
// 使用安全调用语法调用函数
// 使用Elvis运算符处理回调没有被指定的情况
val str = transform?.invoke(element) ?: element.toString()
result.append(str)
}
result.append(postfix)
return result.toString()
}
1.5 返回函数的函数
定义一个返回函数的函数:
enum class Delivery {
STANDARD, EXPEDITED
}
class Order(val itemCount: Int)
fun getShippingCostCalculator(
delivery: Delivery
): (Order) -> Double { // 声明一个返回函数的函数
if (delivery == Delivery.EXPEDITED) {
// 返回lambda
return { order -> 6 + 2.1 * order.itemCount }
}
return { order -> 1.2 * order.itemCount }
}
// 将返回的函数保存在变量中
val calculator = getShippingCostCalculator(Delivery.EXPEDITED)
// 调用返回的函数
println("shipping costs ${calculator(Order(3))}")
// shipping costs 12.3
1.6 通过lambda去除重复代码
data class SiteVisit(
val path: String,
val duration: Double,
val os: OS
)
enum class OS {
IOS, ANDROID
}
首先使用硬编码的过滤器分析数据:
val list = listOf(
SiteVisit("/", 22.0, OS.IOS),
SiteVisit("/", 16.3, OS.ANDROID)
)
val averageIOSDuration = list
.filter { it.os == OS.IOS }
.map(SiteVisit::duration)
.average()
println(averageIOSDuration)
// 22.0
假设需要分析ANDROID数据,为了避免重复,可以抽象一个参数。
用一个普通方法去除重复代码:
// 将重复代码抽取到函数中
fun List<SiteVisit>.averageDurationFor(os: OS) =
filter { it.os == os }.map(SiteVisit::duration).average()
println(list.averageDurationFor(OS.ANDROID))
// 16.3
用一个高阶函数去除重复代码:
fun List<SiteVisit>.averageDurationFor(predicate: (SiteVisit) -> Boolean) =
filter(predicate).map(SiteVisit::duration).average()
println(list.averageDurationFor { it.os in setOf(OS.ANDROID, OS.IOS) })
// 19.15
2. 内联函数:消除lambda带来的运行时开销
lambda表达式会被正常地编译成匿名类。这表示每调用一次lambda表达式,一个额外的类就会被创建,这会带来运行时额外开销。
有没有可能让编译器生成跟java语句同样高效的代码,还能把重复的逻辑抽取到库函数中呢?kotlin的编译器能够做到。如果使用 inline 修饰符标记一个函数,在函数被使用的时候编译器并不会生成函数调用的代码,而是使用函数实现的饿真实代码替换每一次的函数调用。
2.1 内联函数如何运作
当一个函数被声明为inline时,它的函数体是内联的,函数体会被直接替换到函数被调用的地方,而不是被正常调用。
// 定义一个内联函数
inline fun <T> synchronized(lock: Lock, action: () -> T): T {
lock.lock()
try {
return action()
} finally {
lock.unlock()
}
}
val lock = ReentrantLock()
synchronized(lock) {
// ...
}
因为已经将synchronized函数声明为inline,所以每次调用它所生成的代码跟java的synchronized语句是一样的。
fun foo(lock: Lock) {
println("Before sync")
synchronized(lock) {
println("Action")
}
println("After sync")
}
这段代码编译后的foo函数:
fun foo(lock: Lock) {
println("Before sync")
// 被内联的synchronized函数代码
lock.lock()
try {
println("Action") // 被内联的lambda体代码
} finally {
lock.unlock()
}
println("After sync")
}
由lambda生成的字节码成为了函数调用者定义的一部分,而不是被包含在一个实现了函数接口的匿名类中。
在调用内联函数的时,也可以传递函数类型的变量作为参数:
class LockOwner(
val lock: Lock
) {
fun runUnderLock(body: () -> Unit) {
// 传递一个函数类型的变量作为参数,而不是一个lambda
synchronized(lock, body)
}
}
在这种情况下,lambda的代码在内联函数被调用点是不可用的,因此并不会被内联。只有synchronized的函数体被内联了,lambda才会被正常调用。
runUnderLock函数会被编译成类似于以下函数的字节码:
class LockOwner(
val lock: Lock
) {
// 这个函数类似于真正的runUnderLock被编程成的字节码
fun runUnderLock(body: () -> Unit) {
lock.lock()
try {
// body没有被内联,因为在调用的地方还没有lambda
body()
} finally {
lock.unlock()
}
}
}
2.2 内联集合操作
接下来看看kotlin标准库操作集合的函数性能。
举一个例子:使用lambda过滤一个集合
data class Person(
val name: String,
val age: Int
)
val persons = listOf(Person("Alice", 23), Person("Bob", 43))
println(persons.filter { it.age < 30 })
// [Person(name=Alice, age=23)]
在kotlin中,filter函数被声明为内联函数。意味者filter函数,以及传递给它的lambda的字节码会被一起内联到filter被调用的地方。kotlin对内联函数的支持,我们可以不必担心性能的问题。
2.3 决定何时将函数声明成内联
使用inline关键字只能提高带有lambda参数的函数的性能,其它的情况需要额外的研究。
将带有lambda参数的函数内联,能够避免运行时开销。其实不仅节约了函数调用的开销,而且节约了为lambda创建匿名类,以及创建lambda实例对象的开销。
在使用inline关键字的时,注意代码的长度。如果内联函数很大,将它的字节码拷贝到每一个调用点将会极大地增加字节码的长度,应该将与lambda参数无关的代码抽取到一个独立的非内联函数中。
2.4 使用内联lambda管理资源
lambda可以去除重复代码的一个常见模式是资源管理:先获取一个资源,完成一个操作,然后释放资源。资源可以是一个文件,一个锁,一个数据库事务等。模式的标准做法是使用try/finally语句。
如前面实现的synchronized函数。但kotlin标准库定义了另一个叫作withLock函数,它是Lock接口的扩展函数。
val lock : Lock = ReentrantLock()
// 在加锁的情况下执行指定的操作
lock.withLock { // ... }
// 这是Kotlin库中withLock函数的定义:
// 需要加锁的代码抽取到一个独立的方法中
public inline fun <T> Lock.withLock(action: () -> T): T {
lock()
try {
return action()
} finally {
unlock()
}
}
在java中使用try-with-resource语句:
static String readFirstLineFromFile(String path) throws IOException {
try (BufferedReader br = new BufferedReader(new FileReader(path))) {
return br.readLine();
}
}
在kotlin标准库中可以使用use函数:
fun readFirstLineFromFile(path: String): String {
// 构建BufferedReader,调用use函数,传递一个lambda执行文件操作
BufferedReader(FileReader(path)).use { br ->
return br.readLine()
}
}
use函数是一个扩展函数,被用来操作可关闭的资源,它接收一个lambda作为参数。use函数也是内联函数,不会引发任何性能开销。
3. 高阶函数中的控制流
3.1 lambda中的返回语句:从一个封闭的函数返回
来比较两种不同的遍历集合的方法:
data class Person(
val name: String,
val age: Int
)
val persons = listOf(Person("Alice", 23), Person("Bob", 43))
lookForAlice(persons)
fun lookForAlice(persons: List<Person>) {
for (person in persons) {
if (person.name == "Alice") {
println("Found!")
return
}
}
println("Alice is not Found!")
}
// Found!
如果使用forEach迭代重写这段代码安全吗?使用forEach也是安全的。
fun lookForAlice(persons: List<Person>) {
persons.forEach {
if (it.name == "Alice") {
println("Found!")
return
}
}
println("Alice is not Found!")
}
// Found!
如果在lambda中使用return关键字,它会从调用lambda的函数中返回,并不只是从lambda中返回。这样的return语句叫作非局部返回,因为它从一个比包含return的代码块更大的代码块中返回了。
只有在以lambda作为参数的函数是内联函数的时候才能从更外层的函数返回。forEach的函数体和lambda的函数体一起被内联了,所以在编译的时候很容易做到从包含它的函数中返回。在一个非内联函数的lambda中使用return表达式是不允许的。
3.2 从lambda返回:使用标签返回
也可以在lambda表达式中使用局部返回。lambda中的局部返回跟for循环中的break表达式相似。它会终止lambda的执行,并接着从调用lambda的代码处执行。
要区分局部返回还是非局部返回,要用到标签。如果想从一个lambda表达式处返回,可以标记它,然后在return关键字后面引用这个标签。
// 用一个标签实现局部返回
fun lookForAlice(persons: List<Person>) {
// 在lambda表达式上加标签
persons.forEach label@{
if (it.name == "Alice") {
// return@label引用了这个标签
return@label
}
}
println("Alice is not Found!")
}
要标记一个lambda表达式,在lambda的花括号之前放一个标签名(可以是任何标识符),接着跟一个@符号。要从一个lambda返回,在return关键字后跟一个@符号,接着跟标签名。
使用lambda作为参数的函数的函数名可以作为标签:
// 用函数名作为return标签
fun lookForAlice(persons: List<Person>) {
persons.forEach {
if (it.name == "Alice") {
// return@forEach从lambda表达式返回
return@forEach
}
}
println("Alice is not Found!")
}
3.3 匿名函数:默认使用局部返回
// 在匿名函数中使用return
fun lookForAlice(persons: List<Person>) {
// 使用匿名函数取代lambda表达式
persons.forEach(fun(person) {
// return指向最近的函数:一个匿名函数
if (person.name == "Alice") return
println("${person.name} is not Alice!")
})
}
// Bob is not Alice!
匿名函数省略了函数名和参数类型。
// 在filter中使用匿名函数
persons.filter(fun(person): Boolean {
return person.age < 30
})
如果使用表达式函数体,就可以省略返回值类型
// 使用表达式函数体匿名函数
persons.filter(fun(person) = person.age < 30)
在匿名函数中,不带标签的return表达式会从匿名函数返回,而不是从包含匿名函数的函数返回。return从最近的使用fun关键字声明的函数返回。
lambda表达式没有使用fun关键字,所以lambda中的return从最外层的函数返回。匿名函数使用了fun关键字,是从最近fun声明的函数返回。
如果我的文章对您有帮助,不妨点个赞鼓励一下(^_^)
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。