Kotlin教程(八)高階函數(shù)

寫在開頭:本人打算開始寫一個(gè)Kotlin系列的教程,一是使自己記憶和理解的更加深刻,二是可以分享給同樣想學(xué)習(xí)Kotlin的同學(xué)腻异。系列文章的知識(shí)點(diǎn)會(huì)以《Kotlin實(shí)戰(zhàn)》這本書中順序編寫成畦,在將書中知識(shí)點(diǎn)展示出來同時(shí),我也會(huì)添加對(duì)應(yīng)的Java代碼用于對(duì)比學(xué)習(xí)和更好的理解罩驻。

Kotlin教程(一)基礎(chǔ)
Kotlin教程(二)函數(shù)
Kotlin教程(三)類穗酥、對(duì)象和接口
Kotlin教程(四)可空性
Kotlin教程(五)類型
Kotlin教程(六)Lambda編程
Kotlin教程(七)運(yùn)算符重載及其他約定
Kotlin教程(八)高階函數(shù)
Kotlin教程(九)泛型


聲明高階函數(shù)

高階函數(shù)就是以另外一個(gè)函數(shù)作為參數(shù)或者返回值的函數(shù)。在Kotlin中惠遏,函數(shù)可以用lambda或者函數(shù)引用來表示砾跃。因此,任何以lambda或者函數(shù)引用作為參數(shù)的函數(shù)节吮,或者返回值為lambda或函數(shù)引用的函數(shù)抽高,都是高階函數(shù)。例如透绩,標(biāo)準(zhǔn)庫中的filter函數(shù)將一個(gè)判斷式作為參數(shù):

list.filter { x > 0 }

函數(shù)類型

為了聲明一個(gè)以lambda作為實(shí)參的函數(shù)翘骂,你需要知道如何聲明對(duì)應(yīng)形參的類型。在這之前帚豪,我們先來看一個(gè)簡(jiǎn)單的例子碳竟,把lambda表達(dá)式保存在局部變量中。其實(shí)我們已經(jīng)見過在不聲明類型的情況下如何做到這一點(diǎn)狸臣,這依賴于Kotlin的類型推導(dǎo):

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

編譯器推導(dǎo)出sum和action這兩個(gè)變量具有函數(shù)類型∮ㄎΓ現(xiàn)在我們來看看這些變量的顯示類型聲明是什么樣子的:

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

聲明函數(shù)類型,需要將函數(shù)參數(shù)類型放在括號(hào)中烛亦,緊接著是一個(gè)箭頭和函數(shù)的返回類型诈泼。

你應(yīng)該還記得Unit類型用于表示函數(shù)不返回任何有用的值。在聲明一個(gè)普通的函數(shù)時(shí)煤禽,Unit類型的返回值是可以省略的铐达,但是一個(gè)函數(shù)類型聲明總是需要一個(gè)顯式地返回類型,所以這里Unit是不能省略的呜师。

在lambda表達(dá)式{ x, y -> x + y } 中省略參數(shù)了類型娶桦,因?yàn)樗麄兊念愋鸵呀?jīng)在函數(shù)類型的變量聲明部分指定了,不需要在lambda本身的定義中再重復(fù)聲明汁汗。

就像其他方法一樣衷畦,函數(shù)類型的返回值也可以標(biāo)記為可空類型:

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

也可以定義一個(gè)函數(shù)類型的可空變量,為了明確表示是變量本身可空知牌,而不是函數(shù)類型的返回類型可空祈争,你需要將整個(gè)函數(shù)類型的定義包含在括號(hào)內(nèi)并在括號(hào)后面添加一個(gè)問號(hào):

var funOrNull: ((Int, Int) -> Int)? = null

注意這兩個(gè)例子的微妙區(qū)別。如果省略了括號(hào)角寸,聲明的將會(huì)是一個(gè)返回值可空的函數(shù)類型菩混,而不是一個(gè)可空的函數(shù)類型的變量忿墅。

函數(shù)類型的參數(shù)名

可以為函數(shù)類型聲明中的參數(shù)指定名字:

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

>>> val url = "http://kotl.in"
>>> performRequest(url) {code, content -> /*...*/} //可以使用定義的名字
>>> performRequest(url) {code, page -> /*...*/} //也可以改變參數(shù)名字

參數(shù)名稱不會(huì)影響類型的匹配。當(dāng)你聲明一個(gè)lambda時(shí)沮峡,不必使用和函數(shù)類型聲明中一模一樣的參數(shù)名稱疚脐,但命名會(huì)提升代碼可讀性并且能用于IDE的代碼補(bǔ)全。

調(diào)用作為參數(shù)的函數(shù)

知道了怎樣聲明一個(gè)高階函數(shù)邢疙,現(xiàn)在我們拉討論如何去實(shí)現(xiàn)它棍弄。第一個(gè)例子會(huì)盡量簡(jiǎn)單并且使用之前的lambda sum 同樣的聲明。這個(gè)函數(shù)實(shí)現(xiàn)兩個(gè)數(shù)字2和3的任意操作疟游,然后打印結(jié)果呼畸。

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

調(diào)用作為參數(shù)的函數(shù)和調(diào)用普通函數(shù)的語法是一樣的:把括號(hào)放在函數(shù)名后,并把參數(shù)放在括號(hào)內(nèi)颁虐。
來看一個(gè)更有趣的例子蛮原,我們來實(shí)現(xiàn)最常用的標(biāo)準(zhǔn)庫函數(shù):filter函數(shù)。為了讓事情簡(jiǎn)單一點(diǎn)另绩,將實(shí)現(xiàn)基于String類型的filter函數(shù)儒陨,但和作用與幾何的泛型版本的原理是相似的:

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函數(shù)以一個(gè)判斷是作為參數(shù),判斷是的類型是一個(gè)函數(shù)笋籽,以字符作為參數(shù)并返回Boolean類型的值框全。如果讓傳遞給判斷式的字符出現(xiàn)在最終返回的字符串中,判斷式需要返回true干签,反之返回false。
filter函數(shù)的實(shí)現(xiàn)非常簡(jiǎn)單明了拆撼。它檢查每一個(gè)字符是否符合滿足判斷式容劳,如果滿足就將字符添加到包含結(jié)果的StringBuilder中。

在Java中使用函數(shù)類

其背后的原理是闸度,函數(shù)類型被聲明為普通的接口竭贩,一個(gè)函數(shù)類型的變量是FunctionN接口的一個(gè)實(shí)現(xiàn)。Kotlin標(biāo)準(zhǔn)庫定義了一系列的接口莺禁,這些接口對(duì)應(yīng)于不同參數(shù)數(shù)量的函數(shù):Function0<R>沒有參數(shù)的函數(shù)留量、Function1<P1,R>一個(gè)參數(shù)的函數(shù)等等。每個(gè)接口定義了一個(gè)invoke方法哟冬,調(diào)用這個(gè)方法就會(huì)執(zhí)行函數(shù)楼熄。一個(gè)函數(shù)類型的變量就是實(shí)現(xiàn)了對(duì)應(yīng)的FunctionN接口的實(shí)現(xiàn)類的實(shí)例,實(shí)現(xiàn)了類的invoke方法包含了lambda函數(shù)體浩峡。
在Java中可以很簡(jiǎn)單的調(diào)用使用了函數(shù)類型的Kotlin可岂。Java 8的lambda會(huì)被自動(dòng)轉(zhuǎn)換為函數(shù)類型的值。

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

/*Java*/
>>> processTheAnswer(number -> number + 1)
43

在舊版的Java中翰灾,可以傳遞一個(gè)實(shí)現(xiàn)了函數(shù)接口中的invoke方法的匿名類的實(shí)例:

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

在Java中可以很容易地使用Kotlin標(biāo)準(zhǔn)庫中以lambda作為參數(shù)的擴(kuò)展函數(shù)缕粹。但是要注意它們看起來并沒有Kotlin中name直觀——必須顯式地傳遞一個(gè)接收者對(duì)象作為第一個(gè)參數(shù):

    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中稚茅,函數(shù)或者lambda可以返回Unit。但因?yàn)樵贙otlin中Unit類型是有一個(gè)值的平斩,所以需要顯式地返回它亚享。

函數(shù)類型的參數(shù)默認(rèn)值和null

聲明函數(shù)類型的參數(shù)的時(shí)候可以指定參數(shù)的默認(rèn)值。要知道默認(rèn)值的用處绘面,我們回頭看一下教程二中joinToString函數(shù)欺税,以下是它的最終實(shí)現(xiàn):

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()
}

這個(gè)實(shí)現(xiàn)很靈活,但是它并沒有讓你控制轉(zhuǎn)換的關(guān)鍵點(diǎn):集合中元素是如何轉(zhuǎn)換為字符串的飒货。代碼中使用了StringBuilder.append(o: Any?) 魄衅,它總是使用toString方法將對(duì)象轉(zhuǎn)換為字符串。在大多數(shù)情況下這樣就可以了塘辅,但并不總是這樣晃虫。為了解決這個(gè)問題,可以定義一個(gè)函數(shù)類型的參數(shù)并用一個(gè)lambda作為它的默認(rèn)值扣墩。

fun <T> Collection<T>.joinToString(
        separator: String = ",",
        prefix: String = "",
        postfix: String = "",
        transform: (T) -> String = { it.toString() } //默認(rèn)實(shí)現(xiàn)
): String {
    val result = StringBuilder(prefix)
    for ((index, element) in this.withIndex()) {
        if (index > 0) result.append(separator)
        result.append(transform(element)) //使用函數(shù)參數(shù)轉(zhuǎn)換
    }
    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! 

這個(gè)一個(gè)泛型函數(shù):它有一個(gè)類型參數(shù)T表示集合中的元素的類型哲银。transform將接收這個(gè)類型的參數(shù)。
聲明函數(shù)類型的默認(rèn)值并不需要特殊的語法——只需要把lambda作為值放在=號(hào)后面呻惕。上面的例子展示了不同的函數(shù)調(diào)用方式:省略整個(gè)lambda(使用默認(rèn)的toString做轉(zhuǎn)換)荆责,在括號(hào)以外傳遞lambda,或者以命名參數(shù)形式傳遞亚脆。
除了默認(rèn)實(shí)現(xiàn)的方式來達(dá)到選擇性地傳遞做院,另一種選擇是聲明一個(gè)參數(shù)為可空的函數(shù)類型。注意這里不能直接調(diào)用作為參數(shù)傳遞進(jìn)來的函數(shù)濒持,需要先判空:

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

不想判空也是可以键耕,利用函數(shù)類型是一個(gè)包含invoke方法的接口的具體實(shí)現(xiàn)。作為一個(gè)普通方法柑营,invoke可以通過安全調(diào)用語法:callback?.invoke()屈雄。

返回函數(shù)的函數(shù)

從函數(shù)中返回另一個(gè)函數(shù)并沒有將函數(shù)作為參數(shù)傳遞那么常用,但它仍然非常有用官套。想象一下程序中的一段邏輯可能會(huì)因?yàn)槌绦虻臓顟B(tài)或者其他條件而產(chǎn)生變化——比如說酒奶,運(yùn)輸費(fèi)用的計(jì)算依賴于選擇的運(yùn)輸方式∧膛猓可以定義一個(gè)函數(shù)用來選擇恰當(dāng)?shù)倪壿嬜凅w并將它組委另一個(gè)函數(shù)返回惋嚎。

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

聲明一個(gè)返回另一個(gè)函數(shù)的函數(shù),需要指定一個(gè)函數(shù)類型作為返回類型纺阔。getShippingCostCalculator返回了一個(gè)函數(shù)瘸彤,這個(gè)函數(shù)以O(shè)rder作為參數(shù)并返回一個(gè)Double類型的值。要返回一個(gè)函數(shù)笛钝,需要寫一個(gè)return表達(dá)式质况,跟上一個(gè)lambda愕宋、一個(gè)成員引用,或者其他的函數(shù)類型的表達(dá)式结榄,比如一個(gè)函數(shù)類型的局部變量中贝。

通過lambda去除重復(fù)代碼

函數(shù)類型和lambda表達(dá)式一起組成了一個(gè)創(chuàng)建可重用代碼的好工具。
我們來看一個(gè)分析網(wǎng)站訪問的例子臼朗,SiteView類用于保存每次訪問的路徑邻寿。持續(xù)時(shí)間和用戶的操作系統(tǒng)。不同的操作系統(tǒng)使用枚舉類型來表示:

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機(jī)器的平均訪問時(shí)間视哑,可以用average函數(shù)來完成這個(gè)任務(wù):

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

現(xiàn)在假設(shè)你要計(jì)算來自Mac用戶的相同數(shù)據(jù)绣否,為了避免重復(fù),可以將平臺(tái)類型抽象為一個(gè)參數(shù)挡毅。

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

將這個(gè)函數(shù)作為擴(kuò)展函數(shù)增強(qiáng)了可讀性蒜撮。如果它只在局部的上下文中有用,你甚至可以將這個(gè)函數(shù)聲明為局部擴(kuò)展函數(shù)跪呈。
但這還遠(yuǎn)遠(yuǎn)不夠段磨。想像一下,如果你對(duì)來自移動(dòng)平臺(tái)的訪問的平均時(shí)間非常有興趣耗绿。

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

現(xiàn)在已經(jīng)無法再用一個(gè)簡(jiǎn)單的參數(shù)表示不同的平臺(tái)了苹支。你可能還需要使用更加復(fù)雜的條件查詢?nèi)罩尽1热缥笞瑁瑏碜訧OS平臺(tái)對(duì)注冊(cè)頁面的訪問的平均時(shí)間是多少债蜜?Lambda可以幫上忙,可以用函數(shù)類型將需要的條件抽象到一個(gè)參數(shù)中究反。

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

函數(shù)類型可以幫助去除重復(fù)代碼策幼。如果你禁不住復(fù)制粘貼了一段代碼,那么很可能這段重復(fù)的代碼是可以避免的奴紧。使用lambda,不僅可以抽取重復(fù)的數(shù)據(jù)晶丘,也可以抽取重復(fù)的行為黍氮。

一些廣為人知的設(shè)計(jì)模式可以函數(shù)類型和lambda表達(dá)式進(jìn)行簡(jiǎn)化。比如策略模式浅浮。沒有l(wèi)ambda表達(dá)式的情況下沫浆,你需要聲明一個(gè)接口,并為沒一種可能的策略提供實(shí)現(xiàn)類滚秩,使用函數(shù)類型专执,可以用一個(gè)通用的函數(shù)類型來描述策略,然后傳遞不同的lambda表達(dá)式作為不同的策略郁油。

內(nèi)聯(lián)函數(shù):消除lambda帶來的運(yùn)行時(shí)開銷

lambda表達(dá)式會(huì)被正常編譯成匿名類本股。這表示沒調(diào)用一次lambda表達(dá)式攀痊,一個(gè)額外的類就會(huì)被創(chuàng)建。并且如果lambda捕捉了某個(gè)變量拄显,那么每次調(diào)用的時(shí)候都會(huì)創(chuàng)建一個(gè)新的對(duì)象苟径。這會(huì)帶來運(yùn)行時(shí)的額外開銷,導(dǎo)致使用lambda比使用一個(gè)直接執(zhí)行相同代碼的函數(shù)效率更低躬审。
有沒有可能讓編譯器生成跟Java語句同樣高效的代碼棘街,但還是能夠吧重復(fù)的邏輯抽取到庫函數(shù)中呢?是的承边,Kotlin的編譯器能做到遭殉。如果使用inline修飾符標(biāo)記一個(gè)函數(shù),在函數(shù)被使用的時(shí)候編譯器并不會(huì)生成函數(shù)調(diào)用的代碼博助,而是使用函數(shù)實(shí)現(xiàn)的真實(shí)代碼替換每一次的函數(shù)調(diào)用险污。

內(nèi)聯(lián)函數(shù)如何運(yùn)作

當(dāng)一個(gè)函數(shù)被聲明為inline時(shí),它的函數(shù)體是內(nèi)聯(lián)的——換句話說翔始,函數(shù)體會(huì)被直接替換到函數(shù)被調(diào)用的地方罗心,而不是被正常調(diào)用。來看一個(gè)例子以便理解生成的最終代碼城瞎。

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

val l = Lock()
synchronized(l) {...}

這個(gè)函數(shù)用于確保一個(gè)共享資源不會(huì)并發(fā)地被多個(gè)線程訪問渤闷,函數(shù)鎖住一個(gè)Lock對(duì)象,執(zhí)行代碼塊脖镀,然后釋放鎖飒箭。
調(diào)用這個(gè)函數(shù)語法根Java中使用synchronized語句完全一樣。區(qū)別在于Java的synchronized語句可以用于任何對(duì)象蜒灰,而這個(gè)函數(shù)則要求傳入一個(gè)Lock實(shí)例弦蹂。這里展示的定義只是一個(gè)示例,Kotlin標(biāo)準(zhǔn)庫中定義了一個(gè)可以接收任何對(duì)象作為參數(shù)的synchronized函數(shù)的版本强窖。

因?yàn)橐呀?jīng)將synchronized函數(shù)聲明為inline凸椿,所以每次調(diào)用它所生成的代碼跟Java的synchronized語句是一樣的〕崮纾看看下面這個(gè)使用synchronized()的例子:

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

下面的代碼將會(huì)編譯成相同字節(jié)碼:

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

lambda表達(dá)式和synchronized函數(shù)的實(shí)現(xiàn)都被內(nèi)聯(lián)了脑漫。由lambda生成的字節(jié)碼成為了函數(shù)調(diào)用者定義的一部分,而不是被包含在一個(gè)實(shí)現(xiàn)了函數(shù)接口的匿名類中咙崎。
注意优幸,在調(diào)用內(nèi)聯(lián)函數(shù)的時(shí)候也可以傳遞函數(shù)類型的變量作為參數(shù):

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

在這種情況下,lambda的代碼在內(nèi)聯(lián)函數(shù)被調(diào)用點(diǎn)是不可用的褪猛,因此并不會(huì)被內(nèi)聯(lián)网杆。只有synchronized的函數(shù)體被內(nèi)聯(lián)了,lambda才會(huì)被正常調(diào)用。runUnderLock函數(shù)會(huì)被編譯成類似一下函數(shù)的字節(jié)碼:

class LockOwner(val lock: Lock) {
    fun __runUnderLock__(body: () -> Unit) {
        lock.lock()
        try {
            body()  //body沒有被內(nèi)聯(lián)碳却,應(yīng)為在調(diào)用的地方還沒有l(wèi)ambda
        } finally {
            lock.unlock()
        }
    }
}

如果兩個(gè)不同的位置使用同一個(gè)內(nèi)聯(lián)函數(shù)队秩,但是用的時(shí)不同的lambda,那么內(nèi)聯(lián)函數(shù)會(huì)在每一個(gè)被調(diào)用的位置被分別內(nèi)聯(lián)追城。內(nèi)聯(lián)函數(shù)的代碼會(huì)被拷貝到使用它的兩個(gè)不同地方刹碾,并把不同的lambda替換到其中。

內(nèi)聯(lián)函數(shù)的限制

鑒于內(nèi)聯(lián)的運(yùn)作方式座柱,不是所有使用lambda的函數(shù)都可以被內(nèi)聯(lián)迷帜。當(dāng)函數(shù)被內(nèi)聯(lián)的時(shí)候,作為參數(shù)的lambda表達(dá)式的函數(shù)體會(huì)被直接替換到最終生成的代碼中色洞。這將限制函數(shù)體中對(duì)應(yīng)lambda參數(shù)的使用戏锹。如果lambda參數(shù)被調(diào)用,這樣的代碼能被容易地內(nèi)聯(lián)火诸。但如果lambda參數(shù)在某個(gè)地方被保存起來锦针,以便后面可以繼續(xù)使用,lambda表達(dá)式的代碼將不能被內(nèi)聯(lián)置蜀,因?yàn)楸仨氁幸粋€(gè)包含這些代碼的對(duì)象存在奈搜。
一般來說,參數(shù)如果被直接調(diào)用或者作為參數(shù)傳遞給另外一個(gè)inline函數(shù)盯荤,它是可以被內(nèi)聯(lián)的馋吗。否則,編譯器會(huì)禁止參數(shù)被內(nèi)聯(lián)并給出錯(cuò)誤信息“Illegal usage of inline-parameter”秋秤。
例如宏粤,許多作用于序列的函數(shù)會(huì)返回一些類的實(shí)例,這些類代表對(duì)應(yīng)的序列操作并接收lambda作為構(gòu)造方法的參數(shù)灼卢。以下是Sequence.map函數(shù)的定義:

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

map函數(shù)沒有直接調(diào)用作為transform參數(shù)傳遞進(jìn)來的函數(shù)绍哎。而是將這個(gè)函數(shù)傳遞給一個(gè)類的構(gòu)造方法,構(gòu)造方法將它保存在一個(gè)屬性中鞋真。為了支持這一點(diǎn)崇堰,作為transform參數(shù)傳遞的lambda需要被編譯成標(biāo)準(zhǔn)的非內(nèi)聯(lián)的表示法,即一個(gè)實(shí)現(xiàn)了函數(shù)接口的匿名類涩咖。
如果一個(gè)函數(shù)期望兩個(gè)或更多l(xiāng)ambda參數(shù)赶袄,可以選擇只內(nèi)聯(lián)其中一些參數(shù)。這樣是有道理的抠藕,因?yàn)橐粋€(gè)lambda可能會(huì)包含很多代碼或者以不允許內(nèi)聯(lián)的方式使用。接收這樣的非內(nèi)聯(lián)lambda的參數(shù)蒋困,可以用noline修飾符來標(biāo)記它:

inline fun foo(inlined: () -> Unit, noinline notInlined: () -> Unit) {}

編譯器完全支持內(nèi)聯(lián)跨模塊的函數(shù)或者第三方庫定義的函數(shù)盾似。也可以在Java中調(diào)用絕大部分內(nèi)聯(lián)函數(shù),但這些調(diào)用并不會(huì)被內(nèi)聯(lián),而是被編譯成普通的函數(shù)調(diào)用零院。

內(nèi)聯(lián)集合操作

我們來仔細(xì)看一看Kotlin標(biāo)準(zhǔn)庫操作集合函數(shù)的性能溉跃。大部分標(biāo)準(zhǔn)庫中的集合函數(shù)都帶有l(wèi)ambda參數(shù),相比于使用標(biāo)準(zhǔn)庫函數(shù)告抄,直接實(shí)現(xiàn)這些操作不是更高效嗎撰茎?
例如,讓我們來比較以下兩個(gè)代碼中用來過濾一個(gè)人員列表的方式:

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表達(dá)式也可以實(shí)現(xiàn):

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

在Kotlin中打洼,filter函數(shù)被聲明為內(nèi)聯(lián)函數(shù)龄糊。這意味著filter函數(shù),以及傳遞給他的lambda的字節(jié)碼會(huì)被一起內(nèi)聯(lián)戴filter被調(diào)用的地方募疮。最終炫惩,第一種實(shí)現(xiàn)所產(chǎn)生的字節(jié)碼和第二種實(shí)現(xiàn)所產(chǎn)生的字節(jié)碼大致是一樣的。你可以很安全地使用符合語言習(xí)慣的集合操作阿浓,Kotlin對(duì)內(nèi)聯(lián)函數(shù)的支持讓你不必?fù)?dān)心性能問題他嚷。
想象一下現(xiàn)在你聯(lián)系調(diào)用filter和map兩個(gè)操作:

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

這個(gè)例子使用了一個(gè)lambda表達(dá)式和一個(gè)成員引用。再一次芭毙,filter和map函數(shù)都被聲明為inline函數(shù)筋蓖,所以它們的函數(shù)體會(huì)被內(nèi)聯(lián),因此不會(huì)產(chǎn)生額外的類或者對(duì)象退敦。但是上面的代碼卻創(chuàng)建了一個(gè)中間集合來保存列表過濾的結(jié)果粘咖,由filter函數(shù)生成的代碼會(huì)向這個(gè)集合添加元素,而由map函數(shù)生成的代碼會(huì)讀取這個(gè)集合苛聘。
如果有大量集合元素需要處理涂炎,中間集合的運(yùn)行開銷將成為不可忽視的問題,這時(shí)可以在調(diào)用鏈后加上一個(gè)asSquence調(diào)用设哗,用序列來替代集合唱捣。但正如你在前面看到的,用來處理序列的lambda沒有被內(nèi)聯(lián)网梢。每一個(gè)中間序列被表示成把lambda保存在其字段中的對(duì)象震缭,而末端操作會(huì)導(dǎo)致由每一個(gè)中間序列調(diào)用組成的調(diào)用鏈被執(zhí)行。因此战虏,即便序列上的操作是惰性的拣宰,你不應(yīng)該總是試圖在集合操作的調(diào)用鏈后加上asSquence。這只在處理大量數(shù)據(jù)的集合時(shí)有用烦感,曉得集合可以用普通的集合操作處理巡社。

決定何時(shí)將函數(shù)聲明成內(nèi)聯(lián)

inline雖然可以有效減少函數(shù)運(yùn)行時(shí)開銷(包含減少匿名類的創(chuàng)建),但這是基于將標(biāo)記的的函數(shù)拷貝到每一個(gè)調(diào)用點(diǎn)來達(dá)成的手趣,因此晌该,如果函數(shù)體的代碼過多,會(huì)增大字節(jié)碼的大小〕海考慮到JVM本身已經(jīng)提供了強(qiáng)大的內(nèi)聯(lián)支持:它會(huì)分析代碼的執(zhí)行燕耿,并在任何通過內(nèi)聯(lián)能夠帶來好處的時(shí)候?qū)⒑瘮?shù)調(diào)用內(nèi)聯(lián)。還有一點(diǎn)就是Kotlin的內(nèi)聯(lián)函數(shù)在Java調(diào)用時(shí)并沒有其內(nèi)聯(lián)的作用姜胖。最終誉帅,我們應(yīng)該謹(jǐn)慎考慮添加inline,只將一些較小的右莱,并且需要嵌入調(diào)用方的函數(shù)標(biāo)記內(nèi)聯(lián)蚜锨。

高階函數(shù)中的控制流

當(dāng)你開始使用lambda去替換像循環(huán)這樣的命令式代碼結(jié)構(gòu)時(shí),很快便發(fā)現(xiàn)遇到return表達(dá)式的問題隧出。把一個(gè)return語句放在循環(huán)的中間是很簡(jiǎn)單的事情踏志。但是如果將循環(huán)轉(zhuǎn)換成一個(gè)類似filter的函數(shù)呢?在這種情況下return會(huì)如何工作胀瞪?

lambda中的返回語句:從一個(gè)封閉的函數(shù)返回

來比較兩種不同的遍歷集合的方法针余。在下面的代碼中,很明顯如果一個(gè)的名字是Alice凄诞,就應(yīng)該從函數(shù)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語句還會(huì)是一樣的表現(xiàn)嗎?是的帆谍,正如下面的代碼展示的伪朽,forEach是安全的。

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

如果你在lambda中使用return關(guān)鍵字汛蝙,它會(huì)從調(diào)用lambda的函數(shù)中返回烈涮,并不只是從lambda返回。這樣的return語句叫做非局部返回窖剑,因?yàn)樗鼜囊粋€(gè)比包含return的代碼塊更大的代碼塊中返回了坚洽。
為了理解這條規(guī)則背后的邏輯,想想Java函數(shù)中在for循環(huán)或者synchronized代碼塊中使用return關(guān)鍵字西土。顯然會(huì)從函數(shù)中返回讶舰,而不是從循環(huán)或者代碼塊中返回,當(dāng)使用以lambda作為參數(shù)的函數(shù)的時(shí)候Kotlin保留了同樣的行為需了。
需要注意的是跳昼,只有在以lambda作為參數(shù)的函數(shù)是內(nèi)聯(lián)函數(shù)的時(shí)候才能從更外層的函數(shù)返回。上面的例子中forEach的函數(shù)體和lambda的函數(shù)體一起被內(nèi)聯(lián)了肋乍,所以在編譯的時(shí)候很容易做到從包含它的函數(shù)中返回鹅颊。在一個(gè)非內(nèi)斂函數(shù)的lambda中使用return表達(dá)式是不允許的。

從lambda返回:使用標(biāo)簽返回

也可以在lambda表達(dá)式中使用局部返回墓造。lambda中的局部返回跟for循環(huán)中的break表達(dá)式相似堪伍。它會(huì)終止lambda的執(zhí)行历帚,并接著從lambda的代碼處執(zhí)行。要區(qū)分布局返回和非局部返回杠娱,要用到標(biāo)簽。想從一個(gè)lambda表達(dá)式處返回你可以標(biāo)記它谱煤,然后在return關(guān)鍵字后面引用這個(gè)標(biāo)簽摊求。

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

>>> lookForAlice(people)
Alice might be somewhere

要標(biāo)記一個(gè)lambda表達(dá)式,在lambda的花括號(hào)之前放一個(gè)標(biāo)簽名(可以是任何標(biāo)識(shí)符)刘离,接著放一個(gè)@符號(hào)室叉。要從lambda返回,在return關(guān)鍵字后放一個(gè)@符號(hào)硫惕,接著放標(biāo)簽名。
或者默認(rèn)使用lambda作為參數(shù)的函數(shù)的函數(shù)名作為標(biāo)簽:

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

如果你顯式地指定了lambda表達(dá)式的標(biāo)簽,在使用函數(shù)名作為標(biāo)簽沒有任何效果廊遍。一個(gè)lambda表達(dá)式的標(biāo)簽數(shù)量不能多以一個(gè)植捎。

帶標(biāo)簽的this表達(dá)式

同樣的規(guī)則也使用于this表達(dá)式的標(biāo)簽。帶接收者的lambda包含一個(gè)隱式上下文對(duì)象的lambda可以通過this引用去訪問豁辉。如果你給帶接收者的lambda指定標(biāo)簽令野,就可以通過對(duì)應(yīng)的帶標(biāo)簽的this表達(dá)式訪問它的隱式接收者。

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

和return表達(dá)式中使用標(biāo)簽一樣徽级,可以顯示地指定lambda表達(dá)式的標(biāo)簽气破,也可以直接使用函數(shù)名作為標(biāo)簽。

匿名函數(shù):默認(rèn)使用局部返回

匿名函數(shù)是一種不同的用于編寫傳遞給函數(shù)的代碼塊的方式餐抢。先來看一個(gè)示例:

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

匿名函數(shù)看起來跟普通函數(shù)很相似现使,除了它的名字和參數(shù)類型被省略了外。這里有另外一個(gè)例子:

people.filter(fun(person): Boolean {
        return person.age < 30
    })

匿名函數(shù)和普通函數(shù)有相同的指定返回值類型的規(guī)則旷痕。代碼塊體匿名函數(shù)需要顯式地指定返回類型碳锈,如果使用表達(dá)式函數(shù)體,就可以省略返回類型苦蒿。

people.filter(fun(person): Boolean = person.age < 30)

在匿名函數(shù)中殴胧,不帶標(biāo)簽的return表達(dá)式會(huì)從匿名函數(shù)返回,而不是從包含匿名函數(shù)的函數(shù)返回佩迟。這條規(guī)則很簡(jiǎn)單:return從最近使用fun關(guān)鍵字聲明的函數(shù)返回团滥。lambda表達(dá)式?jīng)]有使用fun關(guān)鍵字,所以lambda中的return從最外層的函數(shù)返回报强。匿名函數(shù)使用了fun灸姊,因此,在前一個(gè)例子中匿名函數(shù)是最近的符合規(guī)則的函數(shù)秉溉。所以return表達(dá)式從匿名函數(shù)返回力惯,而不是從最外層的函數(shù)返回碗誉。

注意,盡管匿名函數(shù)看起來跟普通函數(shù)很相似父晶,但它其實(shí)是lambda表達(dá)式的另一種語法形式而已哮缺。關(guān)于lambda表達(dá)式如何實(shí)現(xiàn),以及內(nèi)聯(lián)函數(shù)中如何被內(nèi)聯(lián)的同樣適用于匿名函數(shù)甲喝。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末尝苇,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子埠胖,更是在濱河造成了極大的恐慌糠溜,老刑警劉巖,帶你破解...
    沈念sama閱讀 207,113評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件直撤,死亡現(xiàn)場(chǎng)離奇詭異非竿,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)谋竖,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評(píng)論 2 381
  • 文/潘曉璐 我一進(jìn)店門红柱,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人圈盔,你說我怎么就攤上這事豹芯。” “怎么了驱敲?”我有些...
    開封第一講書人閱讀 153,340評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵铁蹈,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我众眨,道長(zhǎng)握牧,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,449評(píng)論 1 279
  • 正文 為了忘掉前任娩梨,我火速辦了婚禮沿腰,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘狈定。我一直安慰自己颂龙,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,445評(píng)論 5 374
  • 文/花漫 我一把揭開白布纽什。 她就那樣靜靜地躺著措嵌,像睡著了一般。 火紅的嫁衣襯著肌膚如雪芦缰。 梳的紋絲不亂的頭發(fā)上企巢,一...
    開封第一講書人閱讀 49,166評(píng)論 1 284
  • 那天,我揣著相機(jī)與錄音让蕾,去河邊找鬼浪规。 笑死或听,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的笋婿。 我是一名探鬼主播誉裆,決...
    沈念sama閱讀 38,442評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼缸濒!你這毒婦竟也來了找御?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,105評(píng)論 0 261
  • 序言:老撾萬榮一對(duì)情侶失蹤绍填,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后栖疑,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體讨永,經(jīng)...
    沈念sama閱讀 43,601評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,066評(píng)論 2 325
  • 正文 我和宋清朗相戀三年遇革,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了卿闹。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,161評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡萝快,死狀恐怖锻霎,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情揪漩,我是刑警寧澤旋恼,帶...
    沈念sama閱讀 33,792評(píng)論 4 323
  • 正文 年R本政府宣布,位于F島的核電站奄容,受9級(jí)特大地震影響冰更,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜昂勒,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,351評(píng)論 3 307
  • 文/蒙蒙 一蜀细、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧戈盈,春花似錦奠衔、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至血柳,卻和暖如春官册,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背难捌。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評(píng)論 1 261
  • 我被黑心中介騙來泰國(guó)打工膝宁, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留鸦难,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,618評(píng)論 2 355
  • 正文 我出身青樓员淫,卻偏偏與公主長(zhǎng)得像合蔽,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子介返,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,916評(píng)論 2 344

推薦閱讀更多精彩內(nèi)容

  • 前言 人生苦多拴事,快來 Kotlin ,快速學(xué)習(xí)Kotlin圣蝎! 什么是Kotlin刃宵? Kotlin 是種靜態(tài)類型編程...
    任半生囂狂閱讀 26,146評(píng)論 9 118
  • 原文鏈接:https://github.com/EasyKotlin 值就是函數(shù),函數(shù)就是值徘公。所有函數(shù)都消費(fèi)函數(shù)牲证,...
    JackChen1024閱讀 5,952評(píng)論 1 17
  • 寫在開頭:本人打算開始寫一個(gè)Kotlin系列的教程,一是使自己記憶和理解的更加深刻关面,二是可以分享給同樣想學(xué)習(xí)Kot...
    胡奚冰閱讀 1,233評(píng)論 0 6
  • 我看見自己的不確定和迷茫坦袍。 我看見自己又回到了從前,相似的情況等太,相似的選擇捂齐,這是上天對(duì)我的模考嗎缩抡? 我看見自己的恐...
    Echo紫閱讀 187評(píng)論 0 2
  • phobia n.恐懼; 厭惡 anxiety n.焦慮奠宜,憂慮; 切望,渴望; 令人焦慮的事; 掛念 have f...
    Ghosting閱讀 449評(píng)論 0 0