Kotlin教程(六)Lambda編程

寫在開頭:本人打算開始寫一個(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教程(九)泛型


Lambda表達(dá)式,或簡(jiǎn)稱lambda,本質(zhì)上級(jí)就是可以傳遞給其他函數(shù)的一小段代碼。有了lambda,可以輕松地把通用的代碼結(jié)構(gòu)抽取成庫函數(shù),Kotlin標(biāo)準(zhǔn)庫就大量地使用了它們。

Lambda表達(dá)式和成員引用

把lambda引入Java 8是Java這門語言演變過程中讓人望眼欲穿的變化之一。為什么它是如此重要?這一節(jié)中,你會(huì)發(fā)現(xiàn)為何lambda這么好用,以及Kotlin的lambda語法看起來是什么樣子的。

Lambda簡(jiǎn)介:作為函數(shù)參數(shù)的代碼塊

在你代碼中存儲(chǔ)和傳遞一小段行為是常有的任務(wù)。例如,你常常需要表達(dá)像這樣的想法:“當(dāng)一個(gè)時(shí)間發(fā)生的時(shí)候運(yùn)行這個(gè)事件處理器”又或者是“把這個(gè)操作應(yīng)用到這個(gè)數(shù)據(jù)接口中所有元素上”。在老版本的Java中,可以使用匿名內(nèi)部類來實(shí)現(xiàn)。這種技巧可以工作但是語法太啰嗦了。
函數(shù)式編程提供了另外一種解決問題的方法:把函數(shù)當(dāng)做值來對(duì)待??梢灾苯觽鬟f函數(shù),而不需要先聲明一個(gè)類再傳遞這個(gè)類的實(shí)例。使用lambda表達(dá)式之后,代碼會(huì)更加簡(jiǎn)潔。都不需要聲明函數(shù)了,可以高效地直接傳遞代碼塊作為函數(shù)參數(shù)。
我們來看一個(gè)例子。假設(shè)你要定義一個(gè)點(diǎn)擊按鈕的行為,添加一個(gè)負(fù)責(zé)處理點(diǎn)擊的監(jiān)聽器。監(jiān)聽器實(shí)現(xiàn)了相應(yīng)的接口OnClickListener和它的一個(gè)方法onClick:

/* Java */
button.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            //do something
        }
});

這樣聲明匿名內(nèi)部類的寫法實(shí)在是太啰嗦了。在Kotlin中我們可以像Java 8一樣使用lambda來消除這些冗余代碼。

/* Kotlin */
button.setOnClickListener{ /* do someting */ }

這段代碼做了與上面同樣的事情,但是不用再寫啰嗦的匿名內(nèi)部類了。
之前也說過Kotlin可以使用關(guān)鍵字object 匿名內(nèi)部類,因此,你想寫成普通的方式也是可以的:

button.setOnClickListener(object : View.OnClickListener {
            override fun onClick(v: View?) {
                println("on click")
            }
        })

上面兩種方式轉(zhuǎn)換成Java代碼:

button.setOnClickListener((OnClickListener)null.INSTANCE);
button.setOnClickListener((OnClickListener)(new OnClickListener() {
     public void onClick(@Nullable View v) {
        String var2 = "on click";
        System.out.println(var2);
     }
  }));

匿名內(nèi)部類轉(zhuǎn)換成了Java的匿名內(nèi)部類。但是lambda應(yīng)該是Kotlin自己做了特出處理,無法轉(zhuǎn)換成相應(yīng)的Java代碼。

Lambda和集合

我們先來看一個(gè)例子,你會(huì)用到一個(gè)Person類,它包含這個(gè)人的名字和年齡信息:

data class Person(val name: String, val age: Int)

假設(shè)現(xiàn)在你有一個(gè)人的列表,需要找到列表中年齡最大的那個(gè)人。如果完全不了解lambda,你可能會(huì)這樣做:

fun findTheOldest(people: List<Person>) {
    var maxAge = 0
    var theOldest: Person? = null
    for (person in people) {
        if (person.age > maxAge) {
            maxAge = person.age
            theOldest = person
        }
    }
    println(theOldest)
}

>>> val people = listOf(Person("Alice", 29), Person("Hubert", 26))
>>> findTheOldest(people)
Person("Alice", 29)

可以完成目的,但是代碼稍微有點(diǎn)多。而Kotlin有更好的方法,可以使用庫函數(shù):

>>> val people = listOf(Person("Alice", 29), Person("Hubert", 26))
>>> println(people.maxBy{ it.age })
Person("Alice", 29)

maxBy函數(shù)可以在任何集合上調(diào)用,且只需要一個(gè)實(shí)參:一個(gè)函數(shù),指定比較哪個(gè)值來找到最大元素?;ɡㄌ?hào)中的代碼{ it.age } 就是實(shí)現(xiàn)了這個(gè)邏輯的lambda。它接收一個(gè)集合中的元素作為實(shí)參(使用it引用它)并且返回用來比較的值。這個(gè)例子中,集合元素是Person對(duì)象,用來比較的是存儲(chǔ)在其age屬性中的年齡。
如果lambda剛好是函數(shù)或者屬性的委托,可以用成員引用替換:

people.maxBy{ Person::age }

雖然lambda看上去很簡(jiǎn)潔,但是你可能不是很明白到底是如何寫lambda,以及里面的規(guī)則,我們來學(xué)習(xí)下lambda表達(dá)式的語法吧。

Lambda表達(dá)式的語法

一個(gè)lambda把一小段行為進(jìn)行編碼,你能把它當(dāng)做值到處傳遞。它可以被獨(dú)立地聲明并存儲(chǔ)到一個(gè)變量中。但是更常見的還是直接聲明它并傳遞給函數(shù)。

   //參數(shù)           //函數(shù)體
{ x: Int, y: Int -> x + y }

Kotlin的lambda表達(dá)式始終用花括號(hào)包圍。->把實(shí)參和函數(shù)體分割開,左邊是參數(shù)列表,右邊是函數(shù)體。注意參數(shù)并沒有用() 括起來。
可以把lambda表達(dá)式存儲(chǔ)在一個(gè)變量中,把這個(gè)變量當(dāng)做普通函數(shù)對(duì)待(即通過相應(yīng)實(shí)參調(diào)用它):

>>> val sum = {x:Int,y:Int -> x + y}
>>> println(sum(1, 2))
3

如果你樂意,還可以直接調(diào)用lambda表達(dá)式:

>>> { println(42) }()
42

但是這樣的語法毫無可讀性,也沒有什么意義(它等價(jià)于直接執(zhí)行l(wèi)ambda函數(shù)體中的代碼)。如果你確實(shí)需要把一小段代碼封閉在一個(gè)代碼塊中,可以使用庫函數(shù)run來執(zhí)行傳遞它的lambda:

>>> run{ println(42) }
42

在之后的章節(jié)我們會(huì)了解到這種調(diào)用和內(nèi)建語言結(jié)構(gòu)一樣高效且不會(huì)帶來額外運(yùn)行時(shí)開銷,以及背后的原因?,F(xiàn)在我們繼續(xù)看“找到列表中年齡最大”的例子:

>>> val people = listOf(Person("Alice", 29), Person("Hubert", 26))
>>> println(people.maxBy{ it.age })
Person("Alice", 29)

如果不用任何簡(jiǎn)明語法來重寫這個(gè)例子,你會(huì)得到下面的代碼:

people.maxBy({ p: Person -> p.age })

這段代碼一目了然:花括號(hào)中的代碼片段是lambda表達(dá)式,把它作為實(shí)參傳給函數(shù)。這個(gè)lambda接收一個(gè)類型為Person的參數(shù)并返回它的年齡。
但是這段代碼有點(diǎn)啰嗦。首先,過多的標(biāo)點(diǎn)符號(hào)破壞了可讀性。其次,類型可以從上下文推斷出來并可以省略。最后,這種情況下不需要給lambda的參數(shù)分配一個(gè)名稱。
讓我們來改進(jìn)這些地方,先拿花括號(hào)開刀。Kotlin有這樣一種語法約定,如果lambda表達(dá)式是函數(shù)調(diào)用的最后一個(gè)實(shí)參,它可以放到括號(hào)的外邊。這個(gè)例子中,lambda是唯一的實(shí)參,所以可以放到括號(hào)的后邊:

people.maxBy() { p:Person -> p.age }

當(dāng)lambda時(shí)函數(shù)唯一的實(shí)參時(shí),還可以去掉調(diào)用代碼中的空括號(hào):

people.maxBy { p:Person -> p.age }

三種語法形式含義都是一樣的,但最后一種最易讀。如果lambda是唯一的實(shí)參,你當(dāng)然愿意在寫代碼的時(shí)候省掉這些括號(hào)。而當(dāng)你有多個(gè)實(shí)參時(shí),即可以把lambda留在括號(hào)內(nèi)來強(qiáng)調(diào)它是一個(gè)實(shí)參,也可以把它放在括號(hào)的外面,兩種選擇都是可行的。如果你想傳遞兩個(gè)更多的lambda,不能把超過一個(gè)lambda放在外面。
我們來看看這些選項(xiàng)在更復(fù)雜的調(diào)用中是怎樣的。還記得外面在教程二中定義的joinToString函數(shù)嗎?Kotlin標(biāo)準(zhǔn)庫中也有定義它,不同之處在于它可以接收一個(gè)附加的函數(shù)參數(shù)。這個(gè)函數(shù)可以用toString函數(shù)以外的方法來把一個(gè)元素轉(zhuǎn)換成字符串。下面的例子展示了你可以用它只打印出人的名字:

>>> val names = people.joinToString(separator = " ", transform = { p: Person -> p.name })
>>> println(names)
Alice Hubert

這種方式使用命名實(shí)參來傳遞lambda,清楚地表示了lambda應(yīng)用到了哪里。
下面的例子展示課可以怎樣重寫這個(gè)調(diào)用,把lambda放在括號(hào)外:

>>> val names = people.joinToString(" ") { p: Person -> p.name }
>>> println(names)
Alice Hubert

這種方式?jīng)]有顯式地表明lambda引用到了哪里,所以不熟悉被調(diào)用函數(shù)的那些人可能更難理解。

在as或者IDEA中可以使用Alt+Enter喚起操作,使用“Move lambda expression out of parentheses ”把lambda表達(dá)式移動(dòng)到括號(hào)外,或“Move lambda expression into parentheses”把lambda表達(dá)式移動(dòng)到括號(hào)內(nèi)。

我們繼續(xù)簡(jiǎn)化語法,移除參數(shù)的類型。

people.maxBy { p:Person -> p.age }
people.maxBy { p -> p.age }  //推導(dǎo)出參數(shù)類型

和局部變量一樣,如果lambda參數(shù)的類型可以被推導(dǎo)出來,你就不需要顯示地指定它。以這里的maxBy函數(shù)為例,其參數(shù)類型始終和集合的元素類型相同。編譯器知道你是對(duì)一個(gè)Person對(duì)象的集合調(diào)用maxBy函數(shù),所以它能推導(dǎo)lambda參數(shù)也會(huì)是Person類型。
也存在編譯器不能推斷出lambda參數(shù)類型的情況,但這里我們暫不討論。可以遵循這樣的一條簡(jiǎn)單的規(guī)則:先不聲明類型,等編譯器報(bào)錯(cuò)后再來指定它們。
這個(gè)例子你能做的最后簡(jiǎn)化是使用默認(rèn)參數(shù)名稱it代替命名參數(shù)。如果當(dāng)前上下文期望的是只有一個(gè)參數(shù)的lambda且這個(gè)參數(shù)的類型可以推斷出來,就會(huì)生成這個(gè)名稱。

people.maxBy { it.age }  //it是自動(dòng)生成的參數(shù)名稱

僅實(shí)參名稱沒有顯示地指定時(shí)這個(gè)默認(rèn)的名稱才會(huì)生成。

it約定能大大縮短你的代碼,但你不應(yīng)該濫用它。尤其是在嵌套lambda的情況下。最好顯式地聲明每個(gè)lambda的參數(shù)。fouz,很難搞清楚it引用的到底是那個(gè)值。如果上下文中參數(shù)的類型或意義都不是很明朗,顯式聲明參數(shù)的方法也很有效。

如果你用變量存儲(chǔ)lambda,那么就沒有可以推斷出參數(shù)類型的上下文,所以你必須顯式地指定參數(shù)類型:

>>> val getAge = { p:Person -> p.age }
>>> people.maxBy(getAge)

至此你看到的例子都是單個(gè)表達(dá)式或語句構(gòu)成的lambda。但是lambda并沒有 被限制在這樣小的規(guī)模,它可以包含更多的語句。下面這種情況,最后一個(gè)表達(dá)式就是(lambda的)結(jié)果:

val sum = { x: Int, y: Int ->
        println("Computing the sum of $x and $y ...")
        x + y
    }
>>> println(sum(1, 2))
Computing the sum of 1 and 2 ...
3

在作用域中訪問變量

當(dāng)在函數(shù)內(nèi)聲明一個(gè)匿名內(nèi)部類的時(shí)候,能夠在這個(gè)匿名內(nèi)部類引用這個(gè)函數(shù)的參數(shù)和局部變量。也可以用lambda作同樣的事情。如果在函數(shù)內(nèi)部使用lambda,也可以訪問這個(gè)函數(shù)的參數(shù),還有在lambda之前定義的局部變量。
我們用標(biāo)準(zhǔn)庫函數(shù)forEach來展示這種行為。這個(gè)函數(shù)能夠遍歷集合中的每一個(gè)元素,并在該元素上調(diào)用給定的lambda。forEach函數(shù)只是比普通for循環(huán)更簡(jiǎn)潔一些。

fun printMessageWithPrefix(message: Collection<String>, prefix: String) {
    //接受lambda作為實(shí)參指定對(duì)每個(gè)元素的操作
    message.forEach {
        println("$prefix $it")  //在lambda中訪問prefix參數(shù)
    }
}

>>> val errors = listOf("403 Forbidden", "404 Not Found")
>>> printMessageWithPrefix(errors, "Error:")
Error: 403 Forbidden
Error: 404 Not Found

這里Kotlin和Java的一個(gè)顯著區(qū)別是:在Kotlin中不會(huì)僅限于訪問final變量,在lambda內(nèi)部也可以修改這些變量:

fun printProblemCounts(response: Collection<String>) {
    var clientErrors = 0
    var serverErrors = 0
    response.forEach {
        if (it.startsWith("4")) {
            clientErrors++
        } else if (it.startsWith("5")) {
            serverErrors++
        }
    }
    println("$clientErrors client errors, $serverErrors server errors")
}

>>> val response = listOf("200 OK", "418 I'm a teapot", "500 Internal Server Error")
>>> printProblemCounts(response)
1

和Java不一樣,Kotlin允許在lambda內(nèi)部訪問非final變量甚至修改它們。從lambda內(nèi)訪問外部變量,我們稱這些變量被lambda捕獲,就像這個(gè)例子中的prefix、clientErrors以及serverErrors一樣。

訪問非final變量甚至修改它們的原理

注意,默認(rèn)情況下,局部變量的生命周期被限制在聲明這個(gè)變量的函數(shù)中,但是如果它被lambda捕獲了,使用這個(gè)變量的代碼可以被存儲(chǔ)并稍后再執(zhí)行。你可能會(huì)問這事什么原理?當(dāng)你捕獲final變量時(shí),它的值和使用這個(gè)值的lambda代碼一起存儲(chǔ)。而對(duì)非final變量來說,它的值被封裝在一個(gè)特殊的包裝器中,這樣你就可以改變這個(gè)值,而對(duì)這個(gè)包裝器的引用會(huì)和lambda代碼一起存儲(chǔ)。
這個(gè)原理我在教程三中的匿名內(nèi)部類中也有提到:訪問創(chuàng)建匿名內(nèi)部類的函數(shù)中的變量是沒有限制在final變量,當(dāng)時(shí)舉了這個(gè)例子:

var clickCount = 0 
B().setListener(object : Listener {
    override fun onClick() {
        clickCount++ //修改變量
    }
})

并且轉(zhuǎn)換成了Java代碼:

final IntRef clickCount = new IntRef();
clickCount.element = 0;
(new B()).setListener((Listener)(new Listener() {
   public void onClick() {
      int var1 = clickCount.element++;
   }
}));

可以看到真實(shí)被使用clickCount是int類型數(shù),但在Java中使用確實(shí)包裝類IntRef,而真實(shí)int變成了clickCount.element。
任何時(shí)候你捕獲了一個(gè)final變量(val),它的值被拷貝下來,這和Java一樣。而當(dāng)你捕獲了一個(gè)可變變量(var)時(shí),它的值被作為Ref類的一個(gè)實(shí)例被存儲(chǔ)下來。Ref變量是final的能輕易被捕獲,然而實(shí)際值被存儲(chǔ)在其字段中,并且可以在lambda內(nèi)修改。

這里有個(gè)重要的注意事項(xiàng),如果lambda被用作事件處理器或者用在其他異步執(zhí)行的情況,對(duì)局部變量的修改只會(huì)在lambda執(zhí)行的時(shí)候發(fā)生。例如下面這段代碼并不是記錄按鈕點(diǎn)擊次數(shù)的正確方法:

fun tryToCountButtonOnClicks(button: Button): Int {
    var clicks = 0
    button.setOnClickListener { clicks++ }
    return clicks
}

這個(gè)函數(shù)始終返回0。盡管onClick處理器可以修改clicks的值,你并不能觀察到值發(fā)生了變化,因?yàn)閛nClick處理器是在函數(shù)返回之后調(diào)用的。這個(gè)函數(shù)正確的實(shí)現(xiàn)需要把點(diǎn)擊次數(shù)存儲(chǔ)在函數(shù)外依然可以訪問的地方——例如類的屬性,而不是存儲(chǔ)在函數(shù)的局部變量中。

成員引用

你已經(jīng)看到lambda是如何讓你把代碼塊作為參數(shù)傳遞給函數(shù)的。但是如果你想要當(dāng)做參數(shù)傳遞的代碼已經(jīng)被定義成了函數(shù),該怎么辦?當(dāng)然可以傳遞一個(gè)調(diào)用這個(gè)函數(shù)的lambda,但這樣做有點(diǎn)多余。name你能直接傳遞函數(shù)嗎?
Kotlin和Java 8一樣,如果把函數(shù)轉(zhuǎn)換成一個(gè)值,你就可以傳遞它。使用:: 運(yùn)算符來轉(zhuǎn)換:

val getAge = Person::age

這種表達(dá)式稱為成員引用,它提供了簡(jiǎn)明語法,來創(chuàng)建一個(gè)調(diào)用單個(gè)方法或者訪問單個(gè)屬性的函數(shù)值。雙冒號(hào)把類名稱與你要引用的成員(一個(gè)方法或者一個(gè)屬性)名稱隔開。
同樣的內(nèi)容用lambda表達(dá)式實(shí)現(xiàn)是這樣的:

val getAge = { person: Person -> person.age }

不管你引用的函數(shù)還是屬性,都不要在成員引用的名稱后面加括號(hào)。成員引用和調(diào)用該函數(shù)的lambda具有一樣的類型,所以可以互換使用。

還可以引用頂層函數(shù)(不是類的成員):

fun salute() = println("Salute!")
>>> run(::salute)
Salute!

這種情況下,你省略了類名稱,直接以:: 開頭。成員引用::salute 被當(dāng)做實(shí)參傳遞庫函數(shù)run,它會(huì)調(diào)用相應(yīng)的函數(shù)。
如果lambda要委托給一個(gè)接收多個(gè)參數(shù)的函數(shù),提供成員引用代替它將會(huì)非常方便:

val action = { person: Person, message: String ->
    sendEmail(person, message)  //這個(gè)lambda委托給sendEmail函數(shù)
}
val nextAction = ::sendEmail  //可以用成員引用代替

可以用構(gòu)造方法引用存儲(chǔ)或者延期執(zhí)行創(chuàng)建類實(shí)例的作用。構(gòu)造方法引用的形式是在雙冒號(hào)后指定類名稱:

data class Person(val name: String, val age: Int)

>>> val createPerson = ::Person
>>> val p = createPerson("Hubert", 26)  //創(chuàng)建Person實(shí)例的動(dòng)作被保存成了值
>>> println(p)
Person(name=Hubert, age=26)

還可以用同樣的方式引用擴(kuò)展函數(shù):

fun Person.isAdult() = age >= 18
val predicate = Person::isAdult

盡管isAdult不是Person類的成員,還是可以通過引用訪問它,這和訪問實(shí)例的成員沒什么兩樣:person.isAdult()。

綁定引用

在Kotlin 1.0 中,當(dāng)接受一個(gè)類的方法或?qū)傩砸脮r(shí),你始終需要提供一個(gè)該類的實(shí)例來調(diào)用這個(gè)引用。Kotlin 1.1 計(jì)劃支持綁定成員引用,它允許你使用成員引用語法捕獲特定實(shí)例對(duì)象上的方法引用。

>>> val p = Person("Hubert", 26)
>>> val personsAgeFunction = Person::age
>>> println(personsAgeFunction(p))
26
>>> val hubertsAgeFunction = p::age  //Kotlin 1.1 中可以使用綁定成員引用
>>> println(hubertsAgeFunction())
26

注意personsAgeFunction是一個(gè)單參數(shù)函數(shù)(返回給定人的年齡),而hubertsAgeFunction是一個(gè)沒有參數(shù)的函數(shù)(返回p對(duì)象的年齡)。在Kotlin 1.1 之前,你需要顯式地寫出lambda{ p.age } ,而不是使用綁定成員引用p::age。

集合的函數(shù)式API

函數(shù)式編程風(fēng)格在操作集合時(shí)提供了很多優(yōu)勢(shì)。大多數(shù)任務(wù)都可以通過庫函數(shù)完成,來簡(jiǎn)化你的代碼。

filter 和 map

filter和map函數(shù)形成了集合操作的基礎(chǔ),很多集合操作都是借助它們來表達(dá)的。
每個(gè)函數(shù)我們都會(huì)給出兩個(gè)例子,一個(gè)使用數(shù)字,另一個(gè)使用熟悉的Person類:

data class Person(val name: String, val age: Int)

filter函數(shù)遍歷集合并選出應(yīng)用給定lambda后會(huì)返回true的那些元素:

>>> val list = listOf(1, 2, 3, 4)
>>> println(list.filter {  it % 2 == 0 })  //只留下偶數(shù)
[2, 4]

上面的結(jié)果是一個(gè)新的集合,它只包含輸入集合中那些滿足判斷是的元素。
如果你想留下那些超過30歲的人,可以用filter:

>>> val people = listOf(Person("Hubert", 26), Person("Bob", 31))
>>> println(people.filter { it.age > 30 })
Person(name=Bob, age=31)

filter函數(shù)可以從集合中移除你不想要的元素,但是它并不會(huì)改變這些元素。元素的變換是map的用武之地。
map函數(shù)對(duì)集合中的每一個(gè)元素應(yīng)用給定的函數(shù)并把結(jié)果收集到一個(gè)新集合??梢园褦?shù)字列表變換成它們平方的列表,比如:

>>> val list = listOf(1, 2, 3, 4)
>>> println(list.map { it * it })
[1, 4, 9, 16]

結(jié)果是一個(gè)新集合,包含的元素個(gè)數(shù)不變,但是每個(gè)元素根據(jù)給定的判斷式做了變換。
如果你想打印的只是一個(gè)姓名的列表,而不是人的完整信息列表,可以用map來變換列表:

>>> val people = listOf(Person("Hubert", 26), Person("Bob", 31))
>>> println(people.map { it.name })
[Hubert, Bob]

這個(gè)例子也可以同成員引用漂亮地重寫:

people.map(Person::name)

可以輕松地把多次這樣的調(diào)用鏈接起來。例如,打印出年齡超過30歲的人的名字:

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

現(xiàn)在,如果說需要這個(gè)分組中所有年齡最大的人的名字,可以先找到分組中人的最大年齡,然后返回所有這個(gè)年齡的人,很容易就用lambda寫出如下代碼:

people.filter { it.age == people.maxBy(Person::age).age }

但是注意,這段代碼對(duì)每個(gè)人都會(huì)重復(fù)尋找最大年齡的過程,假設(shè)集合中有100個(gè)人,尋找最大年齡的過程就會(huì)執(zhí)行100遍!下面的解決方法做出了改進(jìn),只計(jì)算了一次最大年齡:

val maxAge = people.maxBy(Person::age).age
people.filter { it.age == maxAge }

如果沒有必要就不要重復(fù)計(jì)算!使用lambda表達(dá)式的代碼看起來簡(jiǎn)單,有時(shí)候卻掩蓋底層操作的復(fù)雜性。始終牢記你寫的代碼在干什么。
還可以對(duì)map集合應(yīng)用過濾和變換函數(shù):

>>> val numbers = mapOf(0 to "zero", 1 to "one")
>>> println(numbers.mapValues { it.value.toUpperCase() })
{0=ZERO, 1=ONE}

鍵和值分別由各自的函數(shù)來處理。filterKeys和mapKeys過濾和變換map集合的鍵,而另外的filterValues和mapValues過濾和變換對(duì)應(yīng)的值。

"all" "any" "count"和"find":對(duì)集合應(yīng)用判斷

另一種常見的任務(wù)是檢查集合中所有元素是否都符合某個(gè)條件(或者它的變種,是否存在符合的元素)。Kotlin中,它們是通過all和any函數(shù)表達(dá)的。count函數(shù)檢查有多少元素滿足判斷式,而find函數(shù)返回第一個(gè)符合條件的元素。
為了演示這些函數(shù),我們先來定義一個(gè)判斷式,來檢查一個(gè)人是否還沒有到28歲:

val canBeInClub27 = { p:Person -> p.age <= 27 }

如果你對(duì)是否所有元素都滿足判斷式感興趣,應(yīng)該使用all函數(shù):

>>> val people = listOf(Person("Hubert", 26), Person("Bob", 31))
>>> println(people.all(canBeInClub27))
false

如果你需要檢查集合中是否至少存在一個(gè)匹配的元素,那就用any:

>>> val people = listOf(Person("Hubert", 26), Person("Bob", 31))
>>> println(people.any(canBeInClub27))
true

注意,!all(不是所有)加上某個(gè)條件,可以用any加上這個(gè)條件的取反來替換,反之亦然。為了讓你的代碼更容易理解,應(yīng)該選擇前面不需要否定符號(hào)的函數(shù):

>>> val list = listOf(1, 2, 3)
>>> println(!list.all { it == 3 }) //!否定不明顯,這種情況最好使用any
true
>>> println(list.any { it != 3 })  //lambda參數(shù)中的條件要取反
true

第一行檢查是保證不是所有元素都等于3.這和至少有一個(gè)元素不是3是一個(gè)意思,這正式你在第二行用any做的檢查。
如果你想知道有多少個(gè)元素滿足了判斷式,使用count:

>>> val people = listOf(Person("Hubert", 26), Person("Bob", 31))
>>> println(people.count(canBeInClub27))
1

使用正確的函數(shù)完成工作:count VS size

count方法容易被遺忘,然后通過過濾集合之后再取大小來實(shí)現(xiàn)它:

>>> println(people.filter(canBeInClub27).size)
1

在這種情況下,一個(gè)中間集合會(huì)被創(chuàng)建并用來存儲(chǔ)所有滿足判斷式的元素。而另一方面,count方法只是跟蹤匹配元素的數(shù)量,不關(guān)心元素本身,所以更高效。

要找到一個(gè)滿足判斷式的元素,使用find函數(shù):

>>> val people = listOf(Person("Hubert", 26), Person("Bob", 31))
>>> println(people.find(canBeInClub27))
Person(name=Hubert, age=26)

如果有多個(gè)匹配的元素就返回其中第一個(gè)元素;或者返回null,如果沒有一個(gè)元素能滿足判斷式。find還有一個(gè)同義方法firstOrNull,可以使用這個(gè)方法更清楚地表達(dá)你的意圖。

groupBy:把列表轉(zhuǎn)換成分組的map

假設(shè)你需要把所有元素按照不同的特征劃分成不同的分組。例如,你想把人按年齡分組,相同的年齡的人在一組。把這個(gè)特征直接當(dāng)做參數(shù)傳遞十分方便。groupBy函數(shù)可以幫你做到這一點(diǎn):

>>> val people = listOf(Person("Hubert", 26), Person("Bob", 31), Person("Carol", 31))
>>> println(people.groupBy { it.age })

這次操作的結(jié)果是一個(gè)map,是元素分組依據(jù)的鍵(這個(gè)例子中是age)和元素分組(persons)之間的映射:

{26=[Person(name=Hubert, age=26)], 31=[Person(name=Bob, age=31), Person(name=Carol, age=31)]}

每一個(gè)分組都是存在一個(gè)列表中,結(jié)果的類型就是Map<Int, List<Person>> ??梢允褂孟駇apKeys和mapValues這樣的函數(shù)對(duì)這個(gè)map做進(jìn)一步修改。
我們?cè)賮砜戳硗庖粋€(gè)例子,如何使用成員引用把字符串按照首字母分組:

>>> val list = listOf("a", "ab", "b")
>>> println(list.groupBy(String::first))
{a=[a, ab], b=[b]}

這里的first并不是String類的成員,而是一個(gè)擴(kuò)展,也可以把它當(dāng)做成員引用訪問。

flatmap 和 flatten :處理嵌套集合中的元素

假設(shè)你有一堆書,使用Book類表示:

data class Book(val title: String, val authors: List<String>)

每本書都可能有一個(gè)或者多個(gè)作者,可以統(tǒng)計(jì)出圖書館中的所有作者的set:

books,flatMap { it.authors }.toSet()

flatMap函數(shù)做了兩件事:首先根據(jù)作為實(shí)參給定的函數(shù)對(duì)集合中每個(gè)元素做變換(或者說映射),然后把多個(gè)列表合并(或者說平鋪)成一個(gè)列表。下面這個(gè)字符串的例子很好地闡明了這個(gè)概念:

>>> val strings = listOf("abc", "def")
>>> println(strings.flatMap { it.toList() })
[a, b, c, d, e, f]

字符串上的toList函數(shù)把它轉(zhuǎn)換成字符串列表。如果和toList一起使用的是map函數(shù),你會(huì)得到一個(gè)字符列表的列表,就如同下圖的第二行。flapMap函數(shù)還會(huì)執(zhí)行后面的步驟,并返回一個(gè)包含所有元素的列表。

應(yīng)用flatMap函數(shù)之后的結(jié)果

讓我們回到書籍作者的例子,每一本數(shù)都可能有多位作者,屬性book.authors存儲(chǔ)了每本書籍的作者集合,flatMap函數(shù)把所有書籍作者合并成了一個(gè)扁平的列表。toSet調(diào)用移除了結(jié)果集合中的所有重復(fù)元素。
當(dāng)你卡殼在元素集合不得不合并一個(gè)的時(shí)候,你可能會(huì)想起flapMap來。如果你不需要做任何變換,只是需要平鋪一個(gè)集合,可以使用flatten函數(shù):listOfLists.flatten() 。

惰性集合操作:序列

在上一節(jié),你看到了許多鏈?zhǔn)浇Y(jié)合函數(shù)調(diào)用的例子,比如map和filter。這些函數(shù)會(huì)及早地創(chuàng)建中間集合,也就是說每一步的中間結(jié)果都被存儲(chǔ)在一個(gè)臨時(shí)列表。序列給了你執(zhí)行這些操作的另一種懸著,可以避免創(chuàng)建這些臨時(shí)中間對(duì)象。
先來看個(gè)例子:

people.map(Person::name).filter { it.startsWith("A") }

Kotlin標(biāo)準(zhǔn)庫參考文檔有說明,filter和map都會(huì)返回一個(gè)列表。這意味著上面例子中的鏈?zhǔn)秸{(diào)用會(huì)創(chuàng)建兩個(gè)列表:一個(gè)保存filter函數(shù)的結(jié)果,另一個(gè)保存map函數(shù)的結(jié)果。如果原列表只有兩個(gè)元素,這不是什么問題,但是如果有一百萬個(gè)元素,鏈?zhǔn)秸{(diào)用就會(huì)變得十分低效。
為了提高效率,可以把操作變成使用序列,而不是直接使用集合:

people.asSequence()     //把初始集合轉(zhuǎn)換成序列
            .map(Person::name)
            .filter { it.startsWith("A") }
            .toList()   //把結(jié)果序列轉(zhuǎn)換回列表

應(yīng)用這次操作后的結(jié)果和前面的例子一模一樣:一個(gè)以字母A開頭的人名列表。但是第二個(gè)例子沒有創(chuàng)建任何用于存儲(chǔ)元素的中間集合,所以元素?cái)?shù)量巨大的情況下性能將顯著提升。
Kotlin的惰性集合操作的入口就是Sequence接口。這個(gè)接口表示的就是一個(gè)可以逐個(gè)列舉元素的元素序列。Sequence只提供了一個(gè)方法:iterator,用來從序列中獲取值。
Sequence接口的強(qiáng)大之處在于其操作的實(shí)現(xiàn)方式。序列中的元素求值是惰性的。因此,可以使用序列更高效地對(duì)集合元素執(zhí)行鏈?zhǔn)讲僮?,而不需要穿件額外的集合來保存過程中產(chǎn)生的中間結(jié)果。
可以調(diào)用擴(kuò)展函數(shù)asSequence把任意集合轉(zhuǎn)換成序列,調(diào)用toList來做反向的轉(zhuǎn)換。

執(zhí)行序列操作:中間和末端操作

序列操作分為兩類:中間的和末端的。一次中間操作返回的時(shí)另一個(gè)序列,這個(gè)新序列知道如何變換原始序列中的元素。而一次末端操作返回的是一個(gè)結(jié)果,這個(gè)結(jié)果可能是集合、元素、數(shù)字,或者其他從初始集合的變換序列中獲取的任意對(duì)象。

                    //中間操作         //末端操作
people.asSequence().map{..}.filter {..}.toList() 

中間操作始終都是惰性的。先看看下面這個(gè)缺少了末端操作的例子:

>>> listOf(1, 2, 3, 4).asSequence()
            .map { print("map($it)"); it *it }
            .filter { print("filter($it)");it % 2 == 0 }

執(zhí)行這段代碼并不會(huì)再控制臺(tái)上輸出任何內(nèi)容。這意味著map和filter變換被延期了,它們只有在獲取結(jié)果的時(shí)候才會(huì)被應(yīng)用(即末端操作調(diào)用的時(shí)候):

>>> listOf(1, 2, 3, 4).asSequence()
            .map { print("map($it)"); it *it }
            .filter { print("filter($it)");it % 2 == 0 }
            .toList()
map(1) filter(1) map(2) filter(4) map(3) filter(9) map(4) filter(16)

末端操作觸發(fā)執(zhí)行了所有的延期計(jì)算。
這個(gè)例子中另外一件值得注意的事情是計(jì)算執(zhí)行的順序。一個(gè)笨辦法是現(xiàn)在每個(gè)元素上調(diào)用map函數(shù),然后在結(jié)果序列的每個(gè)元素上在調(diào)用filter函數(shù)。map和filter對(duì)集合就是這樣做的,而序列不一樣。對(duì)序列來說,所有操作是按順序應(yīng)用在每一個(gè)元素上的,處理完第一個(gè)元素(先映射在過濾),然后完成第二個(gè)元素的處理,以此類推。
這種方法意味著部分元素根本不會(huì)發(fā)生任何變換,如果在輪到它們之前就已經(jīng)取得了結(jié)果。我們來看一個(gè)map和find的例子。首先把一個(gè)數(shù)字映射成它的平方,然后找到第一個(gè)比數(shù)字3大的條目:

>>> println(listOf(1, 2, 3, 4)
            .map { print("map($it); "); it * it }
            .find { print("$it > 3 ?; "); it > 3 })
>>> println("----------------")
>>> println(listOf(1, 2, 3, 4).asSequence()
        .map { print("map($it); "); it * it }
        .find { print("$it > 3 ?; "); it > 3 })
        
map(1); map(2); map(3); map(4); 1 > 3 ?; 4 > 3 ?; 4
----------------
map(1); 1 > 3 ?; map(2); 4 > 3 ?; 4

第一種情況,當(dāng)你使用集合的時(shí)候,列表被變換成了另一個(gè)lieb,所以map變換應(yīng)用戴每一個(gè)元素上,包括了數(shù)字3和4.然后第一個(gè)滿足判斷式的元素被找到了:數(shù)字2的平方。
第二種情況,find調(diào)用一開始就逐個(gè)地處理元素。從原始序列中取一個(gè)數(shù)字,用map變換它,然后在檢查它是滿足傳給find的判斷式。當(dāng)進(jìn)行到數(shù)字2時(shí),返現(xiàn)它的平方已經(jīng)比3大,就把它作為find操作結(jié)果返回了。不再需要繼續(xù)檢查數(shù)字3和4,因?yàn)檫@之前你已經(jīng)找到結(jié)果。
在集合上執(zhí)行操作的順序也會(huì)影響性能。假設(shè)你有一個(gè)集合,想要打印集合中哪些長(zhǎng)度小于某個(gè)限制的人名。你需要做兩件事:把每個(gè)人映射成他們的名字,然后過濾掉其中哪些不夠短的名字。這種情況可以用任何順序應(yīng)用map和filter操作。兩種順序得到的結(jié)果一樣,但他們應(yīng)該執(zhí)行的變化總次數(shù)不一樣的:

>>> val people = listOf(Person("Hubert", 26), Person("Alice", 29),
            Person("Bob", 31), Person("Dan", 21))
>>> println(people.asSequence()
       .map { print("map(${it.name}); "); it.name }
        .filter { print("filter($it); ");it.length < 4 }
        .toList())
>>> println("----------------")
>>> println(people.asSequence()
        .filter { print("filter(${it.name}); ");it.name.length < 4 }
        .map { print("map($it); "); it.name }
        .toList())
        

map(Hubert); filter(Hubert); map(Alice); filter(Alice); map(Bob); filter(Bob); map(Dan); filter(Dan); [Bob, Dan]
----------------
filter(Hubert); filter(Alice); filter(Bob); map(Bob); filter(Dan); map(Dan); [Bob, Dan]

可以看到,如果map在前,每個(gè)元素都被變換。而如果filter在前,不合適的元素會(huì)被盡早地過濾掉且不會(huì)發(fā)生變換。

流 VS 序列
如果你很熟悉Java 8 中的流這個(gè)概念,你會(huì)發(fā)現(xiàn)序列就是它的翻版。Kotlin提供了這個(gè)概念自己的版本,原因是Java 8的流并不支持哪些基于Java老版本的平臺(tái),例如Android。如果你的目標(biāo)版本是Java 8,流提供了一個(gè)Kotlin集合和序列目前還沒有實(shí)現(xiàn)的重要特性:在多個(gè)CPU上并行執(zhí)行流操作(比如map和filter)的能力。可以根據(jù)Java的目標(biāo)版本和你的特殊要求在流和序列之間做出選擇。

創(chuàng)建序列

前面的列表都是使用同一個(gè)方法創(chuàng)建序列:在集合上調(diào)用asSquence()。另一個(gè)可能性是使用generateSequence函數(shù)。給定序列中的前一個(gè)元素,這個(gè)函數(shù)會(huì)計(jì)算出下一個(gè)元素。下面這個(gè)例子就是如何使用generateSequence計(jì)算100以內(nèi)所有自然數(shù)之和。

>>> val naturalNumbers = generateSequence(0) { it + 1 }
>>> val numbersTo100 = naturalNumbers.takeWhile { it <= 100 }
>>> println(numbersTo100.sum())
5050

這個(gè)例子中的naturalNumbers和numbersTo100都是有延期操作的序列。這些序列中的實(shí)際數(shù)字直到你調(diào)用末端操作(這里是sum)的時(shí)候才會(huì)求值。
另一種常見的用例是父序列。如果元素的父元素和它的類型相同(比如人類或者Java文件),你可能會(huì)對(duì)它所有祖先組成的序列的特質(zhì)感興趣。下面這個(gè)例子可以查詢文件是否放在隱藏目錄中,通過創(chuàng)建一個(gè)其父類目錄的序列并檢查每個(gè)目錄的屬性來實(shí)現(xiàn)。

fun File.isInsideHiddenDirectory() = 
        generateSequence(this) { it.parentFile }.any{ it.isHidden}
>>> val file = File("/Users/svtk/.HiddenDir/a.txt")
true

你生成一個(gè)序列,通過提供第一個(gè)元素和獲取每個(gè)后續(xù)元素的方式來實(shí)現(xiàn)。如果把a(bǔ)ny換成find,你還可以得到想要的那個(gè)目錄(對(duì)象)。注意,使用序列允許你找到需要的目錄之后立即停止遍歷目錄。

使用Java函數(shù)式接口

Kotlin的lambda也可以無縫地和Java API互操作。在文章開頭,我們就把lambda傳給Java方法的例子:

/* Kotlin */
button.setOnClickListener{ /* do someting */ }

Button類通過接收類型為OnClickListner的實(shí)參的setOnClickListener方法給按鈕設(shè)置一個(gè)新的監(jiān)聽器,在Java(8之前)中我們不得不創(chuàng)建一個(gè)匿名類來作為實(shí)參傳遞:

/* Java */
button.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            //do something
        }
});

在Kotlin中,可以傳遞一個(gè)lambda,代替這個(gè)例子:

/* Kotlin */
button.setOnClickListener{ view -> ... }

這個(gè)lambda用來實(shí)現(xiàn)OnClickListener,它有一個(gè)類型為View的參數(shù),和onclick方法一樣。
這種方法可以工作的原因是OnClickListener接口只有一個(gè)抽象方法。這種接口被稱為函數(shù)式接口,或者SAM接口,SAM代表單抽象方法。Java API 中隨處可見像Runnable和Callable這樣的函數(shù)式接口,以及支持它們的方法。Kotlin允許你在調(diào)用接收函數(shù)式接口作為參數(shù)的方法時(shí)使用lambda,來保證你的Kotlin代碼即整潔又符合習(xí)慣。

和Java不同,Kotlin擁有完全的函數(shù)類型。正因?yàn)檫@樣,需要接收lambda作為參數(shù)的Kotlin函數(shù)應(yīng)該使用函數(shù)類型而不是函數(shù)式接口類型,作為這些參數(shù)的類型。Kotlin不支持把lambda自動(dòng)轉(zhuǎn)換成Kotlin接口對(duì)象。我們會(huì)在之后的章節(jié)中討論聲明函數(shù)類型的用法。

是不是非常好奇把lambda傳給Java時(shí)到底發(fā)生了什么,是如何銜接的?

把lambda當(dāng)做參數(shù)傳遞給Java方法

可以把lambda傳給任何期望函數(shù)式接口的方法。例如下面這個(gè)方法,它有一個(gè)Runnable類型的參數(shù):

/* Java */
void postponeComputation(int delay, Runnable computation);

在Kotlin中,可以調(diào)用它并把一個(gè)lambda作為實(shí)參傳給它。編譯器會(huì)自動(dòng)把它轉(zhuǎn)換成一個(gè)Runnable的實(shí)例。

postponeComputation(1000) { println(42) }

當(dāng)我們說一個(gè)Runnable的實(shí)例時(shí),指的是一個(gè)實(shí)現(xiàn)了Runnable接口的匿名內(nèi)部類的實(shí)例。編譯器會(huì)幫你創(chuàng)建它,并使用lambda作為單抽象方法的方法體。
通過顯示的創(chuàng)建一個(gè)實(shí)現(xiàn)了Runnable的匿名對(duì)象也能達(dá)到同樣的效果:

postponeComputation(1000, object: Runnable {
    override fun run() {
        println(42)
    }
})

但是這里有一點(diǎn)不一樣。當(dāng)你顯式地聲明對(duì)象時(shí),每次調(diào)用都會(huì)創(chuàng)建一個(gè)新的實(shí)例。使用lambda的情況不同:如果lambda沒有訪問任何來自定義它的函數(shù)的變量,相應(yīng)的匿名類實(shí)例可以在多次調(diào)用之間重用。
因此完全等價(jià)的實(shí)現(xiàn)應(yīng)該是下面這段代碼中顯示object聲明,它把Runnable實(shí)例存儲(chǔ)在一個(gè)變量中,并且每次調(diào)用的時(shí)候都使用這個(gè)變量:

val runnable = Runnable { println(42) }
fun handleComputation() {
    postponeComputation(1000, runnable)
}

如果lambda從包圍它的作用域中捕獲了變量,每次調(diào)用就不再可能重用一同一個(gè)實(shí)例了。這種情況下,每次調(diào)用時(shí)編譯器都要?jiǎng)?chuàng)建一個(gè)新對(duì)象,其中存儲(chǔ)著被捕獲的變量的值。

fun handleComputation(id: String) { //lambda會(huì)捕獲id這個(gè)變量
    postponeComputation(1000) { println(id) } //每次都創(chuàng)建一個(gè)Runnable新實(shí)例
}

Lambda的實(shí)現(xiàn)細(xì)節(jié)

自Kotlin 1.0起,每個(gè)lambda表達(dá)式都會(huì)被編譯成一個(gè)匿名類,除非它是一個(gè)內(nèi)聯(lián)lambda。后續(xù)版本計(jì)劃支持生成Java 8字節(jié)碼。一旦實(shí)現(xiàn),編譯器就可以避免為每一個(gè)lambda表達(dá)式都生成一個(gè)獨(dú)立的.class文件。如果lambda捕獲了變量,每個(gè)被捕獲的變量會(huì)在匿名類中有對(duì)應(yīng)的字段,而且每次調(diào)用都會(huì)創(chuàng)建一個(gè)這個(gè)匿名 類的新實(shí)例。否則,一個(gè)單例就會(huì)被創(chuàng)建。類的名稱由lambda聲明所在的函數(shù)名稱加上后綴衍生出來:上面一個(gè)例子就是HandleComputation$1。如果你反編譯之前l(fā)ambda表達(dá)式的代碼,就會(huì)看到:

class HandleComputation$1(val id: String) : Runnable {
    override fun run() {
        println(42)
    }
}
fun handleComputation(id: String) {
    postponeComputation(100, HandleComputation$1(id))
}

如你所見,編譯器給每個(gè)被捕捉的變量生成了一個(gè)字段和一個(gè)構(gòu)造方法參數(shù)。

SAM構(gòu)造方法:顯式地把lambda轉(zhuǎn)換成函數(shù)式接口

SAM構(gòu)造方法是編譯器生成的函數(shù),讓你執(zhí)行從lambda到函數(shù)式接口實(shí)例的顯式轉(zhuǎn)換??梢栽诰幾g器不會(huì)自動(dòng)應(yīng)用轉(zhuǎn)換的上下文中使用它。例如,如果有一個(gè)方法返回的時(shí)一個(gè)函數(shù)式接口的實(shí)例,不能直接返回一個(gè)lambda,要用SAM構(gòu)造方法把它包起來:

fun createAllDoneRunnable(): Runnable {
    return Runnable { println("All Done!") }
}
>>> createAllDoneRunnable().run()
All Done!

SAM構(gòu)造方法的名稱和底層函數(shù)式接口的名稱一樣。SAM構(gòu)造方法只接收一個(gè)參數(shù)——一個(gè)被用作函數(shù)式接口單抽象方法體的lambda,并返回實(shí)現(xiàn)了這個(gè)接口的類的一個(gè)實(shí)例。
除了返回值外,SAM構(gòu)造方法還可以用在需要把從lambda省城的函數(shù)式接口實(shí)例存儲(chǔ)在一個(gè)變量中的情況。假設(shè)你要在多個(gè)按鈕上重用同一個(gè)監(jiān)聽器,就像下面的代碼一樣:

val listener = OnClickListener { view ->
        val text = when(view.id) {
            R.id.button1 -> "First Button"
            R.id.button2 -> "Second Button"
            else -> "Unknown Button"
        }
        toast(text)
}
button1.setOnClickListener(listener)
button2.setOnClickListener(listener)

listener會(huì)檢查哪個(gè)按鈕是點(diǎn)擊事件源并作出相應(yīng)的行為??梢允褂脤?shí)現(xiàn)了OnClickListener的對(duì)象聲明來定義監(jiān)聽器,但是SAM構(gòu)造方法給你更簡(jiǎn)潔的選擇。

Lambda和添加/移除監(jiān)聽器

注意lambda內(nèi)部沒有匿名對(duì)象那樣的this:沒有辦法引用到lambda轉(zhuǎn)換成的匿名類實(shí)例,從編譯器的角度來看,lambda是一個(gè)代碼塊,不是一個(gè)對(duì)象,而且也不能把它當(dāng)成對(duì)象引用。Lambda中的this指向的是包圍它的類。
如果你的事件監(jiān)聽器在處理事件時(shí)還需要取消它自己,不能使用lambda這樣做。這種情況使用實(shí)現(xiàn)了接口的匿名對(duì)象,在匿名對(duì)象內(nèi),this關(guān)鍵字指向該對(duì)象實(shí)例,可以把它傳給移除監(jiān)聽器的API。

帶接收者的lambda:with 與 apply

with 函數(shù)

很多語法都有這樣的語句,可以用它對(duì)同一個(gè)對(duì)象執(zhí)行多次操作,而不需要反復(fù)把對(duì)象的名稱寫出來。Kotlin也不例外,但它提供的是一個(gè)叫with的庫函數(shù),而不是某種特殊的語言結(jié)構(gòu)。
要理解這種用法,我們先看看下面這個(gè)例子,稍后你會(huì)用with來重構(gòu)它:

fun alphabet(): String {
    val result = StringBuilder()
    for (letter in 'A'..'Z') {
        result.append(letter)
    }
    result.append("\nNow I know the alphabet!")
    return result.toString()
}

>>> println(alphabet)
ABCDEFGHIJKLMNOPQRSTUVWXYZ
Now I know the alphabet!

上面這個(gè)例子中,你調(diào)用了result實(shí)例上好幾個(gè)不同的方法,而且每次調(diào)用都要重復(fù)result這個(gè)名稱。這里情況還不算太糟,但是如果你用到的表達(dá)式更長(zhǎng)或者重復(fù)得更多,該怎么辦?
我們來看看使用with函數(shù)重寫這段代碼:

fun alphabet(): String {
    val stringBuilder = StringBuilder()
    return with(stringBuilder) { //指定接收者的值,你會(huì)調(diào)用它的方法
        for (letter in 'A'..'Z') {
            this.append(letter)  //通過顯式地this來調(diào)用接收者值得方法
        } 
        append("\nNow I know the alphabet!") //this可以省略
        this.toString() //從lambda返回
    }
}

with結(jié)構(gòu)看起來像是一個(gè)特殊的語法結(jié)構(gòu),但它實(shí)際上是一個(gè)接收兩個(gè)參數(shù)的函數(shù):這個(gè)例子中兩個(gè)參數(shù)分別是stringBuilder和一個(gè)lambda。這里利用了把lambda放在括號(hào)外的約定,這樣整個(gè)調(diào)用看起來就像是內(nèi)建的語言功能。當(dāng)然你也可以選擇把它寫成with(stringBuilder, {...})
with函數(shù)把它的第一個(gè)參數(shù)轉(zhuǎn)換成第二個(gè)參數(shù)傳給他的lambda的接收者??梢燥@式地通過this引用來訪問這個(gè)接收者?;蛘呖梢允÷詔his引用,不用任何限定符直接訪問這個(gè)值得方法和屬性。這個(gè)例子中this指向了stringBuilder,這是傳給with的第一個(gè)參數(shù)。

帶接收者的lambda和擴(kuò)展函數(shù)

你可能回憶起曾經(jīng)見過相似的概念,this指向的時(shí)函數(shù)接收者。在擴(kuò)展函數(shù)體內(nèi)部,this指向了這個(gè)函數(shù)的那個(gè)類型的實(shí)例,而且也可以被省略掉,讓你直接訪問接收者的成員。一個(gè)擴(kuò)展函數(shù)某種意義上來說就是帶接收者的函數(shù)。

讓我們進(jìn)一步重構(gòu)初始的alphabet函數(shù),去掉額外的stringBuilder變量:

fun alphabet(): String = with(StringBuilder()) {
    for (letter in 'A'..'Z') {
        append(letter)
    }
    append("\nNow I know the alphabet!")
    toString()
}

現(xiàn)在這個(gè)函數(shù)只返回一個(gè)表達(dá)式,所以使用表達(dá)式函數(shù)體語法重寫了它??梢詣?chuàng)建一個(gè)新的StringBuilder實(shí)例直接當(dāng)做實(shí)參傳給這個(gè)函數(shù),然后在lambda中不需要顯示地this就可以引用這個(gè)實(shí)例。

方法名沖突

如果你當(dāng)做參數(shù)傳給with的對(duì)象已經(jīng)有這樣的方法,該方法的名稱和你正在使用with的類中的方法一樣,怎么辦?這種情況下,可以給this引用加上顯式地標(biāo)簽來表明你要調(diào)用的時(shí)哪個(gè)方法。假設(shè)函數(shù)的alphabet是類OuterClass的一個(gè)方法。如果你想引用的是定義在外部類的toString方法而不是StringBuilder,可以用下面這種語句:

this@OuterClass.toString()

with返回的值是執(zhí)行l(wèi)ambda代碼的結(jié)果,該結(jié)果就是lambda中的最后一個(gè)表達(dá)式(的值)。但有時(shí)候你想返回的是接收者對(duì)象,而不是執(zhí)行l(wèi)ambda的結(jié)果。這時(shí)apply庫函數(shù)就派上用場(chǎng)了。

apply 函數(shù)

apply函數(shù)幾乎和with函數(shù)一模一樣,唯一的區(qū)別是apply始終會(huì)返回作為實(shí)參傳遞給它的對(duì)象(接收者對(duì)象)。讓我們?cè)僖淮沃貥?gòu)alphabet函數(shù),這一次用的是apply:

fun alphabet(): String = StringBuilder().apply {
    for (letter in 'A'..'Z') {
        append(letter)
    }
    append("\nNow I know the alphabet!")
}.toString()

apply被聲明成一個(gè)擴(kuò)展函數(shù),他的接收者編程了作為實(shí)參的lambda的接收者。執(zhí)行apply的結(jié)果是StringBuilder,所以接下來你可以調(diào)用toString把它轉(zhuǎn)換成String。
許多情況下apply都很有效,其中一種是在創(chuàng)建一個(gè)對(duì)象實(shí)例需要用正確的方式初始化它的一些屬性的時(shí)候。在Java中,這通常是通過另外一個(gè)單獨(dú)的Builder對(duì)象來完成的,而在Kotlin中,可以在任意對(duì)象上使用apply,完全不需要任何任何來自定義該對(duì)象的庫的特別支持。
我們來用apply演示一個(gè)Android中創(chuàng)建TextView實(shí)例的例子:

fun createViewWithCustomAttr(context: Context) = 
    TextView(context).apply {
        text = "Sample Text"
        textSize = 20.0f
        setPadding(10, 0, 0, 0)
    }

apply函數(shù)允許你使用緊湊的表達(dá)式函數(shù)體風(fēng)格。新的TextView實(shí)例創(chuàng)建之后立即被傳給了apply。在傳給apply的lambda中,TextView實(shí)例變成了接收者,你就可以調(diào)用它的方法并設(shè)置它的屬性。Lambda執(zhí)行之后,apply返回已經(jīng)初始化過的接收者實(shí)例,它變成了createViewWithCustomAttr函數(shù)的結(jié)果。
with函數(shù)和apply函數(shù)是最基本和最通用的使用帶接收者的lambda的例子。更多具體的函數(shù)函數(shù)也可以使用這種模式。例如,你可以使用標(biāo)準(zhǔn)庫函數(shù)buildString進(jìn)一步簡(jiǎn)化alphabet函數(shù),它會(huì)負(fù)責(zé)創(chuàng)建StringBuilder并調(diào)用toString:

fun alphabet(): String = buildString {
    for (letter in 'A'..'Z') {
        append(letter)
    }
    append("\nNow I know the alphabet!")
}
最后編輯于
?著作權(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)容

  • 前言 人生苦多,快來 Kotlin ,快速學(xué)習(xí)Kotlin! 什么是Kotlin? Kotlin 是種靜態(tài)類型編程...
    任半生囂狂閱讀 26,701評(píng)論 9 118
  • lambda即lambda表達(dá)式,簡(jiǎn)稱lambda。本質(zhì)上是可以傳遞給其它函數(shù)的一小段代碼。有了lambda,可以...
    程自舟閱讀 16,248評(píng)論 1 26
  • 原文鏈接:https://github.com/EasyKotlin 值就是函數(shù),函數(shù)就是值。所有函數(shù)都消費(fèi)函數(shù),...
    JackChen1024閱讀 6,346評(píng)論 1 17
  • 不重要的廢話 前段時(shí)間看了一遍《Programming Kotlin》,主要目的是想提高自己的英文閱讀能力,能力提...
    珞澤珈群閱讀 3,576評(píng)論 1 7
  • 文 小Po 看到朋友圈發(fā)的視頻好激動(dòng)好想把它拽過來,搗鼓半天也沒到手,天蝎座的本性讓我固執(zhí)不放棄,心一橫,就...
    小Po兜閱讀 586評(píng)論 0 0

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