Kotlin核心語法(六):高階函數,Lambda做爲形參和返回值

博客主頁java

1. 聲明高階函數

高階函數就是以另外一個函數做爲參數或者返回值的函數。在kotlin中,函數能夠用lambda或函數引用來表示。
例如:標準庫中的filter函數將一個判斷式函數做爲參數,因此就是一個高階函數數據庫

list.filter { x > 0 }

1.1 函數類型

爲了聲明一個以lambda做爲實參的函數,須要知道如何聲明對應的形參的類型。編程

先來看一個簡單的例子:把lambda表達式保存在局部變量中。segmentfault

// 有兩個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是不能省略的。app

在lambda表達式{ x, y -> x + y }中是如何省略參數x,y的類型的呢?由於它們的類型已經在函數類型的變量聲明中指定了,就不須要在lambda定義中重複聲明。ide

函數類型的返回值也能夠標記爲可空類型:函數

var canReturnNull: (Int, Int) -> Int? = { null }

也能夠定義一個函數類型的可空變量:post

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聲明的函數返回。

若是個人文章對您有幫助,不妨點個贊鼓勵一下(^_^)

相關文章
相關標籤/搜索