? 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)庫定義了一系列的接口(Function0,Function1,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è)例子中,filter和map都被聲明為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è)語法形式。