Kotlin學(xué)習(xí)(8)高階函數(shù):lambda作為參數(shù)和返回值

? lambda是用來構(gòu)建抽象的一個(gè)強(qiáng)有力的工具,他們并不局限于集合和標(biāo)準(zhǔn)庫中的類。我們可以將lambda作為函數(shù)參數(shù)和返回值來創(chuàng)建高階函數(shù)。高階函數(shù)可以幫助我們簡(jiǎn)化代碼,移除重復(fù)代碼,以及很好的構(gòu)建抽象。

8.1 聲明高階函數(shù)

? 高階函數(shù)定義為:一個(gè)將另一個(gè)函數(shù)作為參數(shù)或者返回值的函數(shù)。在Kotlin中,函數(shù)可以用lambda或者函數(shù)引用以值的形式來表示。因此,高階函數(shù)就是傳遞lambda或者函數(shù)引用作為參數(shù),或者作為返回值的函數(shù)。例如,filter標(biāo)準(zhǔn)庫函數(shù)是接受一個(gè)predicate函數(shù)作為參數(shù),它就是一個(gè)高階函數(shù)。

val list = listOf(1, 2, 3, 4)
list.filter { it > 2 }
8.1.1 函數(shù)類型

? 為了定義一個(gè)接受lambda作為參數(shù)的函數(shù),我們需要知道如何聲明這個(gè)參數(shù)的類型。首先看一下,直接把lambda保存在一個(gè)變量中的情況。

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

? 這種情況下,編譯器推斷sum變量是有函數(shù)類型的?,F(xiàn)在看一下如何顯式的聲明sum變量的類型

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

? 聲明函數(shù)類型時(shí),需要將函數(shù)參數(shù)類型放在括號(hào)中,將返回值類型放在箭頭后面(Int, Int) -> Int

  • 由于函數(shù)類型聲明中指定了參數(shù)類型,所以lambda中的類型可以省略掉
  • 函數(shù)類型聲明中,函數(shù)返回類型是需要顯式聲明的,所以返回Unit是不可以省略的
  • 函數(shù)類型聲明中,函數(shù)的返回值類型可以是可空的val returnNull: (Int, Int) -> Int? = {_,_ -> null}
  • 當(dāng)然也可以定義一個(gè)可空類型的變量的函數(shù)類型val funOrNull:((Int,Int) ->Int)? = null
8.1.2 調(diào)用函數(shù)作為參數(shù)傳遞

? 現(xiàn)在已經(jīng)知道了如何聲明一個(gè)高階函數(shù),下面自己實(shí)現(xiàn)一個(gè)簡(jiǎn)單的高階函數(shù)。這個(gè)函數(shù)會(huì)對(duì)2和3執(zhí)行一個(gè)任意的操作,并打印操作結(jié)果:

val sum: (Int, Int) -> Int = { x, y -> x + y }
fun numOperation(operation: (Int, Int) -> Int) {
    val result = operation(2, 3)
    println("the result is $result")
}
numOperation(sum)
>>the result is 5

調(diào)用作為參數(shù)傳遞函數(shù)和調(diào)用普通函數(shù)的語法相同:函數(shù)名稱后面加上括號(hào),并將參數(shù)傳遞到括號(hào)中。

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

? Java中使用函數(shù)類型,這其中的機(jī)制是函數(shù)類型被聲明為常規(guī)的接口。根據(jù)參數(shù)數(shù)量的不同分為:Funtion0<R>(無參函數(shù)),Function1<P1,R>(一個(gè)參數(shù)的函數(shù))等等。

? Kotlin中,使用函數(shù)類型的函數(shù)在Java中調(diào)用是很容易的。Java8的lambda會(huì)自動(dòng)轉(zhuǎn)變?yōu)楹瘮?shù)類型的值

//Kotlin中定義高階函數(shù)
fun processAnswer(f: (Int) -> Int) {
    println(f(66))
}
//java中調(diào)用
HighFunc2Kt.processAnswer(number -> number + 1);

? 在老版本的Java中,可以傳遞實(shí)現(xiàn)了對(duì)應(yīng)函數(shù)接口invoke方法的匿名類的一個(gè)實(shí)例

HighFunc2Kt.processAnswer(new Function1<Integer, Integer>() {
    @Override
    public Integer invoke(Integer integer) {
        return integer * 20;
    }
})

? 在Java中,使用Kotlin標(biāo)準(zhǔn)庫中接受lambda作為參數(shù)的擴(kuò)展函數(shù)也是很簡(jiǎn)單的。需要注意的是,你必須顯式將接受者傳遞到第一個(gè)參數(shù)。

List<String> strings = new ArrayList<>();
strings.add("m1Ku");
CollectionsKt.forEach(strings, s -> {
    System.out.print(s);
    return Unit.INSTANCE; //顯式的返回Unit
});
Java中,函數(shù)和lambda可以返回Unit,但是Unit類型在Kotlin中是有值的,所有你需要顯式的返回它。你不能傳遞一個(gè)返回`void`的lambda的傳給一個(gè)期望返回`Unit`的函數(shù)類型。
8.1.4 函數(shù)類型參數(shù)的默認(rèn)值和空值

? 當(dāng)聲明一個(gè)函數(shù)類型的參數(shù)時(shí),我們也可以指定其默認(rèn)值。下面來看一下原來定義過的joinToString函數(shù),這里多加了一個(gè)參數(shù)可以傳遞一個(gè)lambda,用來指定集合中的元素轉(zhuǎn)為String的方式。如果要求調(diào)用時(shí)都傳遞lambda是很麻煩的,這里用一個(gè)默認(rèn)的轉(zhuǎn)換就可以滿足大部分要求。這里就可以為其指定一個(gè)lambda作為默認(rèn)值。

fun <T> Collection<T>.jointToString(
        prefix: String = "{",
        separator: String = ",",
        postfix: String = "}",
        transform: (T) -> String = { it.toString() } //指定函數(shù)類型參數(shù)的默認(rèn)值
): String {
    val stringBuilder = StringBuilder(prefix)
    for ((index, element) in this.withIndex()) {
        if (index > 0) {
            stringBuilder.append(separator)
        }
        stringBuilder.append(transform(element))
    }
    stringBuilder.append(postfix)
    return stringBuilder.toString()
}
val strings = listOf("m1Ku", "alpha", "event")
println(strings.jointToString())
println(strings.jointToString { it.toUpperCase() })
>> {m1Ku,alpha,event}
   {M1KU,ALPHA,EVENT}

? 函數(shù)類型的參數(shù)可以定義為可空的。但是傳遞的可空類型函數(shù)不能在函數(shù)中直接調(diào)用,因?yàn)檫@里有潛在的空指針異常,所以Kotlin會(huì)報(bào)編譯錯(cuò)誤,這里一種選擇是顯式的檢查null:

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

? 這樣寫是有些繁瑣的,但是想寫的簡(jiǎn)潔一些也是完全可能的,這得益于:一個(gè)函數(shù)類型的變量是接口FunctionN的一個(gè)實(shí)現(xiàn)。Kotlin標(biāo)準(zhǔn)庫定義了一系列的接口(Function0Function1,etc),代表擁有不同數(shù)量參數(shù)的函數(shù)。每一個(gè)人接口定義了一個(gè)叫invoke的方法(invoke方法包含lambda體),調(diào)用它就會(huì)執(zhí)行函數(shù)。作為一個(gè)常規(guī)的方法,可以通過安全調(diào)用操作符調(diào)用invoke:callBack?.invoke()

8.1.5 從函數(shù)中返回函數(shù)

? 從一個(gè)函數(shù)中返回另一個(gè)函數(shù)的需求并不如把函數(shù)作為參數(shù)傳遞的需求多,但這個(gè)也是很有用的。例如,程序中的一段代碼邏輯會(huì)跟程序的狀態(tài)或者其他情況而變,即有多種邏輯。舉個(gè)栗子,我們可以根據(jù)選中的運(yùn)費(fèi)計(jì)算方法來計(jì)算運(yùn)費(fèi)。這里定義一個(gè)選擇適當(dāng)?shù)糜?jì)算邏輯的函數(shù),并且將這段邏輯作為函數(shù)返回。

enum class Delivery { 
    STANDARD, EXPEDITED //定義枚舉的運(yùn)輸類型
}
class Order(val goodCount: Int) //訂單類
//傳入運(yùn)輸類型,得到運(yùn)輸費(fèi)的計(jì)算函數(shù)
fun getCostCaculator(delivery: Delivery): (Order) -> Double { 
    if (delivery == Delivery.STANDARD) {
        return { order -> order.goodCount * 10.4 + 10 }
    }
    return { order -> order.goodCount * 25.5 + 20 }
}

val calculator = getCostCalculator(Delivery.STANDARD)
println(calculator(Order(20)))
>>218.0
8.1.6通過lambdas移除重復(fù)代碼

? 函數(shù)類型和lambda表達(dá)式一起組成了一個(gè)很好的創(chuàng)建重用代碼的工具。很多的代碼重復(fù)都可以用lambda表達(dá)式來消滅了。看下面這個(gè)例子:

//定義操作系統(tǒng)
enum class OS { ANDROID, IOS, WINDOWS, MAC }
//定義瀏覽網(wǎng)站的人信息的類
data class SiteVisit(
        val path: String,
        val os: OS,
        val duration: Double
)
//定義信息集合類
val log = listOf(
            SiteVisit("/", OS.ANDROID, 20.2),
            SiteVisit("/user", OS.IOS, 35.3),
            SiteVisit("/vim", OS.ANDROID, 100.43)
    )

? 假設(shè)我們要計(jì)算使用Android瀏覽網(wǎng)站的人的平均使用時(shí)間,可以這樣寫:

log.filter { it.os == OS.ANDROID }
        .map(SiteVisit::duration)
        .average()
當(dāng)然我們可以抽取平臺(tái)類型作為函數(shù)的參數(shù),這樣可以適用于不同的平臺(tái)類型求平均使用時(shí)間。
fun List<SiteVisit>.averageDurationFor(os: OS) =
        filter { it.os == os }
                .map(SiteVisit::duration)
                .average()

? 但是當(dāng)如果我們想求出使用安卓和IOS兩個(gè)移動(dòng)平臺(tái)的人平均使用時(shí)間時(shí),這樣定義函數(shù)就不滿足我們的需求了。此時(shí),我們可以使用函數(shù)類型將使用條件抽取到一個(gè)參數(shù)中。

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

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

? 在學(xué)習(xí)lambda表達(dá)式時(shí),我們已經(jīng)知道lambda正常會(huì)被編譯成匿名類。這就意味著每次使用lambda表達(dá)式時(shí),一個(gè)額外的類都會(huì)被創(chuàng)建;并且如果lambda捕獲了一些變量,每一次調(diào)用lambda時(shí)都會(huì)創(chuàng)建一個(gè)新的對(duì)象。這就帶來了運(yùn)行時(shí)開銷,意味著執(zhí)行相同的代碼使用lambda比函數(shù)效率更低。

? 這里貪心的問一句:是否可以讓編譯器生成和Java一樣高效的代碼,但同時(shí)有可以把重復(fù)邏輯抽取到標(biāo)準(zhǔn)庫中呢?答案是可以的。將函數(shù)以inline關(guān)鍵字修飾,當(dāng)函數(shù)使用時(shí),編譯器不會(huì)生成函數(shù)調(diào)用代碼,而是以真實(shí)的實(shí)現(xiàn)函數(shù)的代碼代替函數(shù)調(diào)用代碼。一臉懵逼,還是試試下面的例子理解吧。

8.2.1 內(nèi)聯(lián)函數(shù)工作原理

? 當(dāng)函數(shù)被聲明為inline時(shí),其函數(shù)體是內(nèi)聯(lián)的,換句話說就是,函數(shù)體會(huì)被直接替換到函數(shù)被調(diào)用的地方,而不是正常的調(diào)用,下面聲明一個(gè)內(nèi)聯(lián)函數(shù):

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

? 在函數(shù)中調(diào)用內(nèi)聯(lián)函數(shù)

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

? 上述調(diào)用與下面的代碼編譯成的字節(jié)碼是相同的

fun _foo(l: Lock) {
    println("before sync")
  //即這里對(duì)synchronized調(diào)用時(shí),直接用synchronized的函數(shù)體做了替換
    try {
        println("Action")
    } finally {
        l.unlock()
    }
    println("After sync")
}
8.2.2 內(nèi)聯(lián)集合操作

? Kotlin標(biāo)準(zhǔn)庫中大部分的集合操作函數(shù)都帶有l(wèi)ambda參數(shù)。但是這里不用擔(dān)心性能問題,由于Kotlin對(duì)內(nèi)聯(lián)函數(shù)的支持,類似filter這種集合操作函數(shù)都會(huì)標(biāo)記為是內(nèi)聯(lián)的。

persons.filter { it.age > 18 }
        .map(Person::name)

上面這個(gè)例子中,filtermap都被聲明為inline的,函數(shù)體都是內(nèi)聯(lián)的,所以不會(huì)產(chǎn)生額外的類或者對(duì)象。但是會(huì)產(chǎn)生一個(gè)集合保存列表過濾的結(jié)果,當(dāng)有集合有大量元素時(shí),可以在集合后加上asSequence,用序列代替集合。對(duì)于小的集合可以使用普通的集合操作處理。

8.2.3 何時(shí)應(yīng)該聲明函數(shù)是內(nèi)聯(lián)的?

? 只有那些接受lambda作為參數(shù)的函數(shù),使用inline關(guān)鍵字標(biāo)記為內(nèi)聯(lián)函數(shù)時(shí)才會(huì)提升性能。對(duì)于常規(guī)的函數(shù)調(diào)用,Java虛擬機(jī)已經(jīng)提供了強(qiáng)大的內(nèi)聯(lián)支持。它會(huì)分析代碼的執(zhí)行,并將調(diào)用內(nèi)聯(lián),這些過程發(fā)生在機(jī)器碼層。在字節(jié)碼上,每個(gè)方法的實(shí)現(xiàn)只會(huì)重復(fù)一次,并不會(huì)因?yàn)椴煌胤椒椒ǖ恼{(diào)用,而多次拷貝代碼。另外如果直接調(diào)用函數(shù),堆棧信息也會(huì)更加清晰。

? 另一方面,接受lambda參數(shù)的內(nèi)聯(lián)函數(shù)是很有效的。第一,使用內(nèi)聯(lián)避免的運(yùn)行時(shí)開銷是很顯著的,節(jié)省的不止是每次調(diào)用,而且還有為lambda創(chuàng)建的額外的來,以及l(fā)ambda實(shí)例的對(duì)象。第二,JVM虛擬機(jī)并不能智能總是執(zhí)行內(nèi)聯(lián)操作。最后,內(nèi)聯(lián)可以讓你使用普通lambda不能使用的特性,比如non-local返回。

? 但是決定使用內(nèi)聯(lián)操作符時(shí)也要注意代碼的體積,當(dāng)想內(nèi)聯(lián)的函數(shù)的體積很大時(shí),就應(yīng)該避免將其定義為內(nèi)聯(lián)函數(shù)?;蛘呤潜M可能抽取和lambda參數(shù)的無關(guān)的代碼到一個(gè)非內(nèi)聯(lián)函數(shù)中。

8.2.4 使用內(nèi)聯(lián)lambda管理資源

? Lambda能夠去除重復(fù)代碼的一個(gè)常見模式是資源管理:在某個(gè)操作前獲取資源,并在結(jié)束后釋放這個(gè)資源。這里的資源可以是一個(gè)文件,一個(gè)鎖,一個(gè)數(shù)據(jù)庫事務(wù)等等。實(shí)現(xiàn)這種模式的標(biāo)準(zhǔn)做法就是使用一個(gè)try/finally語句,在try代碼塊之前獲取資源,并在finally塊中釋放資源。

? 前面我們看到可以將try/finally的邏輯抽取到一個(gè)函數(shù)中,并將使用資源的代碼作為lambda傳遞給函數(shù)。正如前面的我們定義的synchronized(lock: Lock, action: () -> T): T 函數(shù),后面?zhèn)魅氲暮瘮?shù)類型參數(shù)就是需要加鎖執(zhí)行的代碼,而這里的鎖就是前面所說的資源。Kotlin標(biāo)準(zhǔn)庫定義了一個(gè)withLock函數(shù),他是Lock接口的擴(kuò)展函數(shù),它提供了實(shí)現(xiàn)相同功能的更符合語言習(xí)慣的API。

val l: Lock = ...
l.withLock {
//加鎖情況下執(zhí)行指定的代碼
}

? Kotlin庫中withLock函數(shù)的定義:

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

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

? 當(dāng)開始使用lambda代替像循環(huán)這種命令式代碼結(jié)構(gòu)時(shí),你很快就會(huì)遇到return表達(dá)式的問題。在循環(huán)中使用return表達(dá)式是很簡(jiǎn)單的。但是當(dāng)把循環(huán)替換為使用像filter這樣的函數(shù)時(shí),return應(yīng)該怎么用呢?

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

? 在lambda中使用return語句是可以正常返回的,但是他是從調(diào)用lambda的函數(shù)中返回,而不是僅僅是從lambda中返回,這叫非局部返回,因?yàn)樗菑囊粋€(gè)比包含return的代碼塊更大的一個(gè)代碼塊返回了。

fun findPerson(persons: List<Person>) {
    persons.forEach {
        if (it.age > 18){
            println("找到一個(gè)年齡大于18的人${it.name}")
            return
        }
    }
    println("沒有找到年齡大于18的人")
}
findPerson(persons)
>>找到一個(gè)年齡大于18的人m1Ku
8.3.2 從lambda返回:使用標(biāo)簽返回

? 在lambda中也是可以實(shí)現(xiàn)局部返回的,局部返回類似于for循環(huán)的break語句。即結(jié)束lambda中代碼的執(zhí)行,繼續(xù)執(zhí)行調(diào)用lambda的函數(shù)的函數(shù)體中的代碼。要區(qū)分局部返回和非局部返回,要使用標(biāo)簽。想從一個(gè)lambda表達(dá)式返回,可以標(biāo)記它,然后再return關(guān)鍵字后面引用這個(gè)標(biāo)簽。

? 想要標(biāo)記一個(gè)局部返回的lambda,需要在lambda的花括號(hào)前寫上標(biāo)簽名(可以任意起的)并在標(biāo)簽名后面加@符號(hào),并在返回語句時(shí)引用這個(gè)標(biāo)簽,例如:

fun findPerson(persons: List<Person>) {
    persons.forEach label@ { //標(biāo)記一個(gè)叫l(wèi)abel的標(biāo)簽
        if (it.age > 18) {
            println("找到一個(gè)年齡大于18的人${it.name}")
            return@label //引用這個(gè)標(biāo)簽并且返回
        }
    }
    println("沒有找到年齡大于18的人")
}
//此時(shí),程序始終會(huì)輸出>>沒有找到年齡大于18的人

? 另一種選擇是,將使用lambda作為參數(shù)的函數(shù)的函數(shù)名作為標(biāo)簽。

persons.forEach label@ {
    if (it.age > 18) {
        println("找到一個(gè)年齡大于18的人${it.name}")
        return@forEach
    }
}

如果顯式的給定了標(biāo)簽的名字,就不能再使用函數(shù)的名字作為標(biāo)簽了。另外,lambda表達(dá)式中只能有一個(gè)標(biāo)簽。

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

? 另一種給函數(shù)傳遞代碼片段的方式是使用匿名函數(shù)。

? 這里給forEach傳遞一個(gè)匿名函數(shù)代替lambda,匿名函數(shù)的函數(shù)名和參數(shù)類型可以省略。

fun findM1Ku(persons: List<Person>) {
    persons.forEach(fun(person) {
        if (person.name == "m1Ku") return
        println("person ${person.name} is not m1Ku")
    })
}

? 匿名函數(shù)聲明返回值類型的規(guī)則和常規(guī)函數(shù)相同,有函數(shù)體的函數(shù)需要顯式的聲明返回值的類型。表達(dá)式體的函數(shù)可以省略掉返回值類型。

persons.filter(fun(person): Boolean {
    return person.age > 18
})  

? 匿名函數(shù)的return是從匿名函數(shù)局部返回,而不是包圍它的函數(shù)。這里的規(guī)則很簡(jiǎn)單:return會(huì)從最近的使用fun關(guān)鍵字聲明的函數(shù)中返回。由于lambda表達(dá)式不使用fun關(guān)鍵字,所以lambda中的retun會(huì)從外層函數(shù)返回。

? 需要注意的是:盡管匿名函數(shù)看起來和普通函數(shù)很像,但它只是lambda表達(dá)式另一個(gè)語法形式。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容