寫在開頭:本人打算開始寫一個(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ù)甲喝。