博客主页

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声明的函数返回。

如果我的文章对您有帮助,不妨点个赞鼓励一下(^_^)


小兵兵同学
56 声望23 粉丝

Android技术分享平台,每个工作日都有优质技术文章分享。从技术角度,分享生活工作的点滴。