Kotlin教程(八)高階函數

寫在開頭:本人打算開始寫一個Kotlin系列的教程,一是使本身記憶和理解的更加深入,二是能夠分享給一樣想學習Kotlin的同窗。系列文章的知識點會以《Kotlin實戰》這本書中順序編寫,在將書中知識點展現出來同時,我也會添加對應的Java代碼用於對比學習和更好的理解。編程

Kotlin教程(一)基礎
Kotlin教程(二)函數
Kotlin教程(三)類、對象和接口
Kotlin教程(四)可空性
Kotlin教程(五)類型
Kotlin教程(六)Lambda編程
Kotlin教程(七)運算符重載及其餘約定
Kotlin教程(八)高階函數
Kotlin教程(九)泛型設計模式


聲明高階函數

高階函數就是以另一個函數做爲參數或者返回值的函數。在Kotlin中,函數能夠用lambda或者函數引用來表示。所以,任何以lambda或者函數引用做爲參數的函數,或者返回值爲lambda或函數引用的函數,都是高階函數。例如,標準庫中的filter函數將一個判斷式做爲參數:安全

list.filter { x > 0 }
複製代碼

函數類型

爲了聲明一個以lambda做爲實參的函數,你須要知道如何聲明對應形參的類型。在這以前,咱們先來看一個簡單的例子,把lambda表達式保存在局部變量中。其實咱們已經見過在不聲明類型的狀況下如何作到這一點,這依賴於Kotlin的類型推導:bash

val sum = { x: Int, y: Int -> x + y }
val action = { println(42) }
複製代碼

編譯器推導出sum和action這兩個變量具備函數類型。如今咱們來看看這些變量的顯示類型聲明是什麼樣子的:併發

val sum: (Int, Int) -> Int = { x, y -> x + y }
val action: () -> Unit = { println(42) }
複製代碼

聲明函數類型,須要將函數參數類型放在括號中,緊接着是一個箭頭和函數的返回類型。app

你應該還記得Unit類型用於表示函數不返回任何有用的值。在聲明一個普通的函數時,Unit類型的返回值是能夠省略的,可是一個函數類型聲明老是須要一個顯式地返回類型,因此這裏Unit是不能省略的。ide

在lambda表達式{ x, y -> x + y } 中省略參數了類型,由於他們的類型已經在函數類型的變量聲明部分指定了,不須要在lambda自己的定義中再重複聲明。函數

就像其餘方法同樣,函數類型的返回值也能夠標記爲可空類型:工具

var canReturnNull: (Int, Int) -> Int? = { null }
複製代碼

也能夠定義一個函數類型的可空變量,爲了明確表示是變量自己可空,而不是函數類型的返回類型可空,你須要將整個函數類型的定義包含在括號內並在括號後面添加一個問號:佈局

var funOrNull: ((Int, Int) -> Int)? = null
複製代碼

注意這兩個例子的微妙區別。若是省略了括號,聲明的將會是一個返回值可空的函數類型,而不是一個可空的函數類型的變量。

函數類型的參數名

能夠爲函數類型聲明中的參數指定名字:

fun performRequest(
        url: String,
        callback: (code: Int, content: String) -> Unit //給函數類型的參數定義名字
) {
    /*...*/
}

>>> val url = "http://kotl.in"
>>> performRequest(url) {code, content -> /*...*/} //可使用定義的名字
>>> performRequest(url) {code, page -> /*...*/} //也能夠改變參數名字
複製代碼

參數名稱不會影響類型的匹配。當你聲明一個lambda時,沒必要使用和函數類型聲明中如出一轍的參數名稱,但命名會提高代碼可讀性而且能用於IDE的代碼補全。

調用做爲參數的函數

知道了怎樣聲明一個高階函數,如今咱們拉討論如何去實現它。第一個例子會盡可能簡單而且使用以前的lambda sum 一樣的聲明。這個函數實現兩個數字2和3的任意操做,而後打印結果。

fun twoAndThree(operation: (Int, Int) -> Int) {
    val result = operation(2, 3)
    println("The result is $result")
}

>>> twoAndThree { a, b -> a + b }
The result is 5
>>> twoAndThree { a, b -> a * b }
The result is 6
複製代碼

調用做爲參數的函數和調用普通函數的語法是同樣的:把括號放在函數名後,並把參數放在括號內。
來看一個更有趣的例子,咱們來實現最經常使用的標準庫函數:filter函數。爲了讓事情簡單一點,將實現基於String類型的filter函數,但和做用與幾何的泛型版本的原理是類似的:

fun String.filter(predicate: (Char) -> Boolean): String {
    val sb = StringBuilder()
    for (index in 0 until length) {
        val element = get(index)
        if (predicate(element)) sb.append(element)
    }
    return sb.toString()
}
複製代碼

filter函數以一個判斷是做爲參數,判斷是的類型是一個函數,以字符做爲參數並返回Boolean類型的值。若是讓傳遞給判斷式的字符出如今最終返回的字符串中,判斷式須要返回true,反之返回false。
filter函數的實現很是簡單明瞭。它檢查每個字符是否符合知足判斷式,若是知足就將字符添加到包含結果的StringBuilder中。

在Java中使用函數類

其背後的原理是,函數類型被聲明爲普通的接口,一個函數類型的變量是FunctionN接口的一個實現。Kotlin標準庫定義了一系列的接口,這些接口對應於不一樣參數數量的函數:Function0<R>沒有參數的函數、Function1<P1,R>一個參數的函數等等。每一個接口定義了一個invoke方法,調用這個方法就會執行函數。一個函數類型的變量就是實現了對應的FunctionN接口的實現類的實例,實現了類的invoke方法包含了lambda函數體。
在Java中能夠很簡單的調用使用了函數類型的Kotlin。Java 8的lambda會被自動轉換爲函數類型的值。

/*Kotlin定義*/
fun processTheAnswer(f: (Int) -> Int) {
    println(f(42))
}

/*Java*/
>>> processTheAnswer(number -> number + 1)
43
複製代碼

在舊版的Java中,能夠傳遞一個實現了函數接口中的invoke方法的匿名類的實例:

processTheAnswer(new Function1<Integer, Integer>() {
            @Override
            public Integer invoke(Integer integer) {
                System.out.println(integer);
                return integer + 1;
            }
        });
複製代碼

在Java中能夠很容易地使用Kotlin標準庫中以lambda做爲參數的擴展函數。可是要注意它們看起來並無Kotlin中name直觀——必須顯式地傳遞一個接收者對象做爲第一個參數:

public static void main(String[] args) {
        List<String> strings = new ArrayList<>();
        strings.add("42");
        CollectionsKt.forEach(strings, new Function1<String, Unit>() {
            @Override
            public Unit invoke(String s) {
                System.out.println(s);
                return Unit.INSTANCE;
            }
        });
    }
    
//輸出
42
複製代碼

在Java中,函數或者lambda能夠返回Unit。但由於在Kotlin中Unit類型是有一個值的,因此須要顯式地返回它。

函數類型的參數默認值和null

聲明函數類型的參數的時候能夠指定參數的默認值。要知道默認值的用處,咱們回頭看一下教程二中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)
        result.append(element)
    }
    result.append(postfix)
    return result.toString()
}
複製代碼

這個實現很靈活,可是它並無讓你控制轉換的關鍵點:集合中元素是如何轉換爲字符串的。代碼中使用了StringBuilder.append(o: Any?) ,它老是使用toString方法將對象轉換爲字符串。在大多數狀況下這樣就能夠了,但並不老是這樣。爲了解決這個問題,能夠定義一個函數類型的參數並用一個lambda做爲它的默認值。

fun <T> Collection<T>.joinToString(
        separator: String = ",",
        prefix: String = "",
        postfix: String = "",
        transform: (T) -> String = { it.toString() } //默認實現
): String {
    val result = StringBuilder(prefix)
    for ((index, element) in this.withIndex()) {
        if (index > 0) result.append(separator)
        result.append(transform(element)) //使用函數參數轉換
    }
    result.append(postfix)
    return result.toString()
}

fun main(args: Array<String>) {
    val letters = listOf("Alpha", "Beta")
    println(letters.joinToString())
    println(letters.joinToString { it.toLowerCase() })
    println(letters.joinToString(separator = "! ", postfix = "! ", transform = { it.toUpperCase() }))
}

//輸出
Alpha,Beta
alpha,beta
ALPHA! BETA! 
複製代碼

這個一個泛型函數:它有一個類型參數T表示集合中的元素的類型。transform將接收這個類型的參數。
聲明函數類型的默認值並不須要特殊的語法——只須要把lambda做爲值放在=號後面。上面的例子展現了不一樣的函數調用方式:省略整個lambda(使用默認的toString作轉換),在括號之外傳遞lambda,或者以命名參數形式傳遞。
除了默認實現的方式來達到選擇性地傳遞,另外一種選擇是聲明一個參數爲可空的函數類型。注意這裏不能直接調用做爲參數傳遞進來的函數,須要先判空:

fun foo(callback: (() -> Unit)?){
    if (callback != null) {
        callback()
    }
}
複製代碼

不想判空也是能夠,利用函數類型是一個包含invoke方法的接口的具體實現。做爲一個普通方法,invoke能夠經過安全調用語法:callback?.invoke()

返回函數的函數

從函數中返回另外一個函數並無將函數做爲參數傳遞那麼經常使用,但它仍然很是有用。想象一下程序中的一段邏輯可能會由於程序的狀態或者其餘條件而產生變化——好比說,運輸費用的計算依賴於選擇的運輸方式。能夠定義一個函數用來選擇恰當的邏輯變體並將它組委另外一個函數返回。

enum class Delivery {STANDARD, EXPEDITED }

class Order(val itemCount: Int)

fun getShippingCostCalculator(delivery: Delivery): (Order) -> Double {
    if (delivery == Delivery.EXPEDITED) {
        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
複製代碼

聲明一個返回另外一個函數的函數,須要指定一個函數類型做爲返回類型。getShippingCostCalculator返回了一個函數,這個函數以Order做爲參數並返回一個Double類型的值。要返回一個函數,須要寫一個return表達式,跟上一個lambda、一個成員引用,或者其餘的函數類型的表達式,好比一個函數類型的局部變量。

經過lambda去除重複代碼

函數類型和lambda表達式一塊兒組成了一個建立可重用代碼的好工具。
咱們來看一個分析網站訪問的例子,SiteView類用於保存每次訪問的路徑。持續時間和用戶的操做系統。不一樣的操做系統使用枚舉類型來表示:

enum class OS {WINDOWS, LINUX, MAC, IOS, ANDROID }

data class SiteVisit(val path: String, val duration: Double, val os: OS)

val log = listOf(SiteVisit("/", 34.0, OS.WINDOWS),
            SiteVisit("/", 22.0, OS.MAC),
            SiteVisit("/login", 12.0, OS.WINDOWS),
            SiteVisit("/signup", 8.0, OS.IOS),
            SiteVisit("/", 16.3, OS.ANDROID))
複製代碼

想象一下若是你須要顯示來自Windows機器的平均訪問時間,能夠用average函數來完成這個任務:

val averageWindowsDuration =
            log.filter { it.os == OS.WINDOWS }
                    .map(SiteVisit::duration)
                    .average()
                    
>>> println(averageWindowsDuration)
23.0
複製代碼

如今假設你要計算來自Mac用戶的相同數據,爲了不重複,能夠將平臺類型抽象爲一個參數。

fun List<SiteVisit>.averageDurationFor(os: OS) =
        filter { it.os == os }.map { it.duration }.average()
        
>>> println(log.averageDurationFor(OS.WINDOWS))
23.0
>>> println(log.averageDurationFor(OS.MAC))
22.0
複製代碼

將這個函數做爲擴展函數加強了可讀性。若是它只在局部的上下文中有用,你甚至能夠將這個函數聲明爲局部擴展函數。
但這還遠遠不夠。想像一下,若是你對來自移動平臺的訪問的平均時間很是有興趣。

val averageMoblieDuration =
            log.filter { it.os in setOf(OS.IOS, OS.ANDROID) }
                    .map(SiteVisit::duration)
                    .average()
        
>>> println(averageMoblieDuration)
12.15
複製代碼

如今已經沒法再用一個簡單的參數表示不一樣的平臺了。你可能還須要使用更加複雜的條件查詢日誌。好比,來自IOS平臺對註冊頁面的訪問的平均時間是多少?Lambda能夠幫上忙,能夠用函數類型將須要的條件抽象到一個參數中。

fun List<SiteVisit>.averageDurationFor(predicate: (SiteVisit) -> Boolean)
        = filter(predicate).map(SiteVisit::duration).average()

>>> println(log.averageDurationFor {it.os in setOf(OS.ANDROID, OS.IOS)})
12.15
>>> println(log.averageDurationFor {it.os ==OS.IOS && it.path == "/signup"})
8.0
複製代碼

函數類型能夠幫助去除重複代碼。若是你禁不住複製粘貼了一段代碼,那麼極可能這段重複的代碼是能夠避免的。使用lambda,不只能夠抽取重複的數據,也能夠抽取重複的行爲。

一些廣爲人知的設計模式能夠函數類型和lambda表達式進行簡化。好比策略模式。沒有lambda表達式的狀況下,你須要聲明一個接口,併爲沒一種可能的策略提供實現類,使用函數類型,能夠用一個通用的函數類型來描述策略,而後傳遞不一樣的lambda表達式做爲不一樣的策略。

內聯函數:消除lambda帶來的運行時開銷

lambda表達式會被正常編譯成匿名類。這表示沒調用一次lambda表達式,一個額外的類就會被建立。而且若是lambda捕捉了某個變量,那麼每次調用的時候都會建立一個新的對象。這會帶來運行時的額外開銷,致使使用lambda比使用一個直接執行相同代碼的函數效率更低。
有沒有可能讓編譯器生成跟Java語句一樣高效的代碼,但仍是可以吧重複的邏輯抽取到庫函數中呢?是的,Kotlin的編譯器能作到。若是使用inline修飾符標記一個函數,在函數被使用的時候編譯器並不會生成函數調用的代碼,而是使用函數實現的真實代碼替換每一次的函數調用。

內聯函數如何運做

當一個函數被聲明爲inline時,它的函數體是內聯的——換句話說,函數體會被直接替換到函數被調用的地方,而不是被正常調用。來看一個例子以便理解生成的最終代碼。

inline fun <T> synchronized(lock: Lock, action: () -> T): T {
    lock.lock()
    try {
        return action()
    } finally {
        lock.unlock()
    }
}

val l = Lock()
synchronized(l) {...}
複製代碼

這個函數用於確保一個共享資源不會併發地被多個線程訪問,函數鎖住一個Lock對象,執行代碼塊,而後釋放鎖。
調用這個函數語法根Java中使用synchronized語句徹底同樣。區別在於Java的synchronized語句能夠用於任何對象,而這個函數則要求傳入一個Lock實例。這裏展現的定義只是一個示例,Kotlin標準庫中定義了一個能夠接收任何對象做爲參數的synchronized函數的版本。

由於已經將synchronized函數聲明爲inline,因此每次調用它所生成的代碼跟Java的synchronized語句是同樣的。看看下面這個使用synchronized()的例子:

fun foo(l: Lock) {
    println("Before sync")
    synchronized(l) {
        println("Action")
    }
    println("After sync")
}
複製代碼

下面的代碼將會編譯成相同字節碼:

fun __foo__(l: Lock) {
   println("Before sync")
   l.lock()
    try {
        println("Action")
    } finally {
        l.unlock()
    }
    println("After sync")
}
複製代碼

lambda表達式和synchronized函數的實現都被內聯了。由lambda生成的字節碼成爲了函數調用者定義的一部分,而不是被包含在一個實現了函數接口的匿名類中。
注意,在調用內聯函數的時候也能夠傳遞函數類型的變量做爲參數:

class LockOwner(val lock: Lock) {
    fun runUnderLock(body: () -> Unit) {
        synchronized(lock, body)
    }
}
複製代碼

在這種狀況下,lambda的代碼在內聯函數被調用點是不可用的,所以並不會被內聯。只有synchronized的函數體被內聯了,lambda纔會被正常調用。runUnderLock函數會被編譯成相似一下函數的字節碼:

class LockOwner(val lock: Lock) {
    fun __runUnderLock__(body: () -> Unit) {
        lock.lock()
        try {
            body()  //body沒有被內聯,應爲在調用的地方尚未lambda
        } finally {
            lock.unlock()
        }
    }
}
複製代碼

若是兩個不一樣的位置使用同一個內聯函數,可是用的時不一樣的lambda,那麼內聯函數會在每個被調用的位置被分別內聯。內聯函數的代碼會被拷貝到使用它的兩個不一樣地方,並把不一樣的lambda替換到其中。

內聯函數的限制

鑑於內聯的運做方式,不是全部使用lambda的函數均可以被內聯。當函數被內聯的時候,做爲參數的lambda表達式的函數體會被直接替換到最終生成的代碼中。這將限制函數體中對應lambda參數的使用。若是lambda參數被調用,這樣的代碼能被容易地內聯。但若是lambda參數在某個地方被保存起來,以便後面能夠繼續使用,lambda表達式的代碼將不能被內聯,由於必需要有一個包含這些代碼的對象存在。
通常來講,參數若是被直接調用或者做爲參數傳遞給另一個inline函數,它是能夠被內聯的。不然,編譯器會禁止參數被內聯並給出錯誤信息「Illegal usage of inline-parameter」。
例如,許多做用於序列的函數會返回一些類的實例,這些類表明對應的序列操做並接收lambda做爲構造方法的參數。如下是Sequence.map函數的定義:

public fun <T, R> Sequence<T>.map(transform: (T) -> R): Sequence<R> {
    return TransformingSequence(this, transform)
}
複製代碼

map函數沒有直接調用做爲transform參數傳遞進來的函數。而是將這個函數傳遞給一個類的構造方法,構造方法將它保存在一個屬性中。爲了支持這一點,做爲transform參數傳遞的lambda須要被編譯成標準的非內聯的表示法,即一個實現了函數接口的匿名類。
若是一個函數指望兩個或更多lambda參數,能夠選擇只內聯其中一些參數。這樣是有道理的,由於一個lambda可能會包含不少代碼或者以不容許內聯的方式使用。接收這樣的非內聯lambda的參數,能夠用noline修飾符來標記它:

inline fun foo(inlined: () -> Unit, noinline notInlined: () -> Unit) {}
複製代碼

編譯器徹底支持內聯跨模塊的函數或者第三方庫定義的函數。也能夠在Java中調用絕大部份內聯函數,但這些調用並不會被內聯,而是被編譯成普通的函數調用。

內聯集合操做

咱們來仔細看一看Kotlin標準庫操做集合函數的性能。大部分標準庫中的集合函數都帶有lambda參數,相比於使用標準庫函數,直接實現這些操做不是更高效嗎?
例如,讓咱們來比較如下兩個代碼中用來過濾一我的員列表的方式:

data class Person(val name: String, val age: Int)

val people = listOf(Person("Hubert", 26), Person("Bob", 31))
>>> println(people.filter{ it.age <= 30 })
[Person(name=Hubert, age=26)]
複製代碼

前面的代碼不用lambda表達式也能夠實現:

val result = mutableListOf<Person>()
for (person in people) {
    if (person.age <= 30) result.add(person)
}
println(result)
複製代碼

在Kotlin中,filter函數被聲明爲內聯函數。這意味着filter函數,以及傳遞給他的lambda的字節碼會被一塊兒內聯戴filter被調用的地方。最終,第一種實現所產生的字節碼和第二種實現所產生的字節碼大體是同樣的。你能夠很安全地使用符合語言習慣的集合操做,Kotlin對內聯函數的支持讓你沒必要擔憂性能問題。
想象一下如今你聯繫調用filter和map兩個操做:

>>> println(people.filter { it.age > 30 }.map(Person::name))
[Bob]
複製代碼

這個例子使用了一個lambda表達式和一個成員引用。再一次,filter和map函數都被聲明爲inline函數,因此它們的函數體會被內聯,所以不會產生額外的類或者對象。可是上面的代碼卻建立了一箇中間集合來保存列表過濾的結果,由filter函數生成的代碼會向這個集合添加元素,而由map函數生成的代碼會讀取這個集合。
若是有大量集合元素須要處理,中間集合的運行開銷將成爲不可忽視的問題,這時能夠在調用鏈後加上一個asSquence調用,用序列來替代集合。但正如你在前面看到的,用來處理序列的lambda沒有被內聯。每個中間序列被表示成把lambda保存在其字段中的對象,而末端操做會致使由每個中間序列調用組成的調用鏈被執行。所以,即使序列上的操做是惰性的,你不該該老是試圖在集合操做的調用鏈後加上asSquence。這隻在處理大量數據的集合時有用,曉得集合能夠用普通的集合操做處理。

決定什麼時候將函數聲明成內聯

inline雖然能夠有效減小函數運行時開銷(包含減小匿名類的建立),但這是基於將標記的的函數拷貝到每個調用點來達成的,所以,若是函數體的代碼過多,會增大字節碼的大小。考慮到JVM自己已經提供了強大的內聯支持:它會分析代碼的執行,並在任何經過內聯可以帶來好處的時候將函數調用內聯。還有一點就是Kotlin的內聯函數在Java調用時並無其內聯的做用。最終,咱們應該謹慎考慮添加inline,只將一些較小的,而且須要嵌入調用方的函數標記內聯。

高階函數中的控制流

當你開始使用lambda去替換像循環這樣的命令式代碼結構時,很快便發現遇到return表達式的問題。把一個return語句放在循環的中間是很簡單的事情。可是若是將循環轉換成一個相似filter的函數呢?在這種狀況下return會如何工做?

lambda中的返回語句:從一個封閉的函數返回

來比較兩種不一樣的遍歷集合的方法。在下面的代碼中,很明顯若是一個的名字是Alice,就應該從函數lookForAlice返回:

fun main(args: Array<String>) {
    val people = listOf(Person("Alice", 29), Person("Bob", 31))
    lookForAlice(people)
}

data class Person(val name: String, val age: Int)

fun lookForAlice(people: List<Person>) {
    for (person in people) {
        if (person.name == "Alice") {
            println("Found!")
            return
        }
    }
    println("Alice is not found")
}

//輸出
Found!
複製代碼

使用forEach迭代重寫這段代碼安全嗎?return語句還會是同樣的表現嗎?是的,正以下面的代碼展現的,forEach是安全的。

fun lookForAlice(people: List<Person>) {
    people.forEach {
        if (it.name == "Alice") {
            println("Found!")
            return
        }
    }
    println("Alice is not found")
}
複製代碼

若是你在lambda中使用return關鍵字,它會從調用lambda的函數中返回,並不僅是從lambda返回。這樣的return語句叫作非局部返回,由於它從一個比包含return的代碼塊更大的代碼塊中返回了。
爲了理解這條規則背後的邏輯,想一想Java函數中在for循環或者synchronized代碼塊中使用return關鍵字。顯然會從函數中返回,而不是從循環或者代碼塊中返回,當使用以lambda做爲參數的函數的時候Kotlin保留了一樣的行爲。
須要注意的是,只有在以lambda做爲參數的函數是內聯函數的時候才能從更外層的函數返回。上面的例子中forEach的函數體和lambda的函數體一塊兒被內聯了,因此在編譯的時候很容易作到從包含它的函數中返回。在一個非內斂函數的lambda中使用return表達式是不容許的。

從lambda返回:使用標籤返回

也能夠在lambda表達式中使用局部返回。lambda中的局部返回跟for循環中的break表達式類似。它會終止lambda的執行,並接着從lambda的代碼處執行。要區分佈局返回和非局部返回,要用到標籤。想從一個lambda表達式處返回你能夠標記它,而後在return關鍵字後面引用這個標籤。

fun lookForAlice(people: List<Person>) {
    people.forEach label@{  //聲明標籤
        if (it.name == "Alice") {
            return@label  //返回標籤
        }
    }
    println("Alice might be somewhere")
}

>>> lookForAlice(people)
Alice might be somewhere
複製代碼

要標記一個lambda表達式,在lambda的花括號以前放一個標籤名(能夠是任何標識符),接着放一個@符號。要從lambda返回,在return關鍵字後放一個@符號,接着放標籤名。
或者默認使用lambda做爲參數的函數的函數名做爲標籤:

fun lookForAlice(people: List<Person>) {
    people.forEach {
        if (it.name == "Alice") {
            return@forEach
        }
    }
    println("Alice might be somewhere")
}
複製代碼

若是你顯式地指定了lambda表達式的標籤,在使用函數名做爲標籤沒有任何效果。一個lambda表達式的標籤數量不能多以一個。

帶標籤的this表達式

一樣的規則也使用於this表達式的標籤。帶接收者的lambda包含一個隱式上下文對象的lambda能夠經過this引用去訪問。若是你給帶接收者的lambda指定標籤,就能夠經過對應的帶標籤的this表達式訪問它的隱式接收者。

println(StringBuilder().apply sb@ {
        listOf(1, 2, 3).apply {
            this@sb.append(this.toString())
        }
    })
複製代碼

和return表達式中使用標籤同樣,能夠顯示地指定lambda表達式的標籤,也能夠直接使用函數名做爲標籤。

匿名函數:默認使用局部返回

匿名函數是一種不一樣的用於編寫傳遞給函數的代碼塊的方式。先來看一個示例:

fun lookForAlice(people: List<Person>) {
    people.forEach(
            fun(person) {  //使用匿名函數取代lambda
                if (person.name == "Alice") {
                    return
                }
                println("${person.name} is not Alice")
            }
    )
}
>>> lookForAlice(people)
Bob is not Alice
複製代碼

匿名函數看起來跟普通函數很類似,除了它的名字和參數類型被省略了外。這裏有另一個例子:

people.filter(fun(person): Boolean {
        return person.age < 30
    })
複製代碼

匿名函數和普通函數有相同的指定返回值類型的規則。代碼塊體匿名函數須要顯式地指定返回類型,若是使用表達式函數體,就能夠省略返回類型。

people.filter(fun(person): Boolean = person.age < 30)
複製代碼

在匿名函數中,不帶標籤的return表達式會從匿名函數返回,而不是從包含匿名函數的函數返回。這條規則很簡單:return從最近使用fun關鍵字聲明的函數返回。lambda表達式沒有使用fun關鍵字,因此lambda中的return從最外層的函數返回。匿名函數使用了fun,所以,在前一個例子中匿名函數是最近的符合規則的函數。因此return表達式從匿名函數返回,而不是從最外層的函數返回。

注意,儘管匿名函數看起來跟普通函數很類似,但它實際上是lambda表達式的另外一種語法形式而已。關於lambda表達式如何實現,以及內聯函數中如何被內聯的一樣適用於匿名函數。

相關文章
相關標籤/搜索