Kotlin教程(八)高階函式

胡奚冰發表於2018-04-04

寫在開頭:本人打算開始寫一個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的型別推導:

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) }
複製程式碼

宣告函式型別,需要將函式引數型別放在括號中,緊接著是一個箭頭和函式的返回型別。

你應該還記得Unit型別用於表示函式不返回任何有用的值。在宣告一個普通的函式時,Unit型別的返回值是可以省略的,但是一個函式型別宣告總是需要一個顯式地返回型別,所以這裡Unit是不能省略的。

在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表示式如何實現,以及行內函數中如何被內聯的同樣適用於匿名函式。

相關文章