前序
??????在Kotlin中德玫,函數作為一等公民存在,函數可以像值一樣被傳遞展哭。lambda就是將一小段代碼封裝成匿名函數湃窍,以參數值的方式傳遞到函數中,供函數使用匪傍。
初識lambda
??????在Java8之前您市,當外部需要設置一個類中某種事件的處理邏輯時,往往需要定義一個接口(類)役衡,并創(chuàng)建其匿名實例作為參數茵休,具體的處理邏輯存放到某個對應的方法中來實現:
mName.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
}
});
但Kotlin說,太TM啰嗦了,我直接將處理邏輯(代碼塊)傳遞給你:
mName.setOnClickListener {
}
??????上面的語法為Kotlin的lambda表達式手蝎,都說lambda是匿名函數榕莺,匿名是知道了,但參數列表和返回類型呢柑船?那如果這樣寫呢:
val sum = { x:Int, y:Int ->
x + y
}
??????lambda表達式始終用花括號包圍帽撑,并用 -> 將參數列表和函數主體分離。當lambda自行進行類型推導時鞍时,最后一行表達式返回值類型作為lambda的返回值類型】骼現在一個函數必需的參數列表、函數體和返回類型都一一找出來了逆巍。
函數類型
??????都說可以將函數作為變量值傳遞及塘,那該變量的類型如何定義呢?函數變量的類型統稱函數類型锐极,所謂函數類型就是聲明該函數的參數類型列表和函數返回值類型笙僚。
先看個簡單的函數類型:
() -> Unit
??????函數類型和lambda一樣,使用 -> 作分隔符灵再,但函數類型是將參數類型列表和返回值類型分開肋层,所有函數類型都有一個圓括號括起來的參數類型列表和返回值類型。
一些相對簡單的函數類型:
//無參翎迁、無返回值的函數類型(Unit 返回類型不可省略)
() -> Unit
//接收T類型參數栋猖、無返回值的函數類型
(T) -> Unit
//接收T類型和A類型參數、無返回值的函數類型(多個參數同理)
(T,A) -> Unit
//接收T類型參數汪榔,并且返回R類型值的函數類型
(T) -> R
//接收T類型和A類型參數蒲拉、并且返回R類型值的函數類型(多個參數同理)
(T,A) -> R
較復雜的函數類型:
(T,(A,B) -> C) -> R
一看有點復雜,先將(A,B) -> C抽出來,當作一個函數類型Y,Y = (A,B) -> C,整個函數類型就變成(T,Y) -> R雌团。
??????當顯示聲明lambda的函數類型時燃领,可以省去lambda參數列表中參數的類型,并且最后一行表達式的返回值類型必須與聲明的返回值類型一致:
val min:(Int,Int) -> Int = { x,y ->
//只能返回Int類型锦援,最后一句表達式的返回值必須為Int
//if表達式返回Int
if (x < y){
x
}else{
y
}
}
??????掛起函數屬于特殊的函數類型猛蔽,掛起函數的函數類型中擁有 suspend 修飾符 ,例如 suspend () -> Unit 或者 suspend A.(B) -> C雨涛。(掛機函數屬于協程的知識枢舶,可以暫且放過)
類型別名
??????類型別名為現有類型提供替代名稱。如果類型名稱太長替久,可以另外引入較短的名稱凉泄,并使用新的名稱替代原類型名。類型別名不會引入新類型蚯根,它等效于相應的底層類型后众。使用類型別名為函數類型起別稱:
typealias alias = (String,(Int,Int) -> String) -> String
typealias alias2 = () -> Unit
除了函數類型外,也可以為其他類型起別名:
typealias FileTable<K> = MutableMap<K, MutableList<File>>
lambda語句簡化
??????由于Kotlin會根據上下文進行類型推導颅拦,我們可以使用更簡化的lambda蒂誉,來實現更簡潔的語法。以maxBy函數為例距帅,該函數接受一個函數類型為(T) -> R的參數:
data class Person(val age:Int,val name:String)
val persons = listOf(Person(17,"daqi"),Person(20,"Bob"))
//尋找年齡最大的Person對象
//花括號的代碼片段代表lambda表達式右锨,作為參數傳遞到maxBy()方法中。
persons.maxBy( { person: Person -> person.age } )
- 當lambda表達式作為函數調用的最后一個實參碌秸,可以將它放在括號外邊:
persons.maxBy() { person: Person ->
person.age
}
persons.joinToString (" "){person ->
person.name
}
- 當lambda是函數唯一的實參時绍移,還可以將函數的空括號去掉:
persons.maxBy{ person: Person ->
person.age
}
- 跟局部變量一樣,lambda參數的類型可以被推導處理讥电,可以不顯式的指定參數類型:
persons.maxBy{ person ->
person.age
}
??????因為maxBy()函數的聲明蹂窖,參數類型始終與集合的元素類型相同,編譯器知道你對Person集合調用maxBy函數恩敌,所以能推導出lambda表達式的參數類型也是Person瞬测。
public inline fun <T, R : Comparable<R>> Iterable<T>.maxBy(selector: (T) -> R): T? {
}
??????但如果使用函數存儲lambda表達式,則無法根據上下文推導出參數類型纠炮,這時必須顯式指定參數類型月趟。
val getAge = { p:Person -> p.age }
//或顯式指定變量的函數類型
val getAge:(Person) -> Int = { p -> p.age }
- 當lambda表達式中只有一個參數,沒有顯示指定參數名稱恢口,并且這個參數的類型能推導出來時狮斗,會生成默認參數名稱it
persons.maxBy{
it.age
}
??????默認參數名稱it雖然簡潔,但不能濫用弧蝇。當多個lambda嵌套的情況下,最好顯式地聲明每個lambda表達式的參數,否則很難搞清楚it引用的到底是什么值看疗,嚴重影響代碼可讀性沙峻。
var persons:List<Person>? = null
//顯式指定參數變量名稱,不使用it
persons?.let { personList ->
personList.maxBy{ person ->
person.age
}
}
- 可以把lambda作為命名參數傳遞
persons.joinToString (separator = " ",transform = {person ->
person.name
})
- 當函數需要兩個或以上的lambda實參時两芳,不能把超過一個的lambda放在括號外面摔寨,這時使用常規(guī)傳參語法來實現是最好的選擇。
SAM 轉換
??????回看剛開始的setOnClickListener()方法,那接收的參數是一個接口實例怖辆,不是函數類型呀是复!怎么就可以傳lambda了呢?先了解一個概念:函數式接口:
函數式接口就是只定義一個抽象方法的接口
??????SAM轉換就是將lambda顯示轉換為函數式接口實例竖螃,但要求Kotlin的函數類型和該SAM(單一抽象方法)的函數類型一致淑廊。SAM轉換一般都是自動發(fā)生的。
??????SAM構造方法是編譯器為了將lambda顯示轉換為函數式接口實例而生成的函數特咆。SAM構造函數只接收一個參數 —— 被用作函數式接口單抽象方法體的lambda季惩,并返回該函數式接口的實例。
SAM構造方法的名稱和Java函數式接口的名稱一樣腻格。
顯示調用SAM構造方法画拾,模擬轉換:
#daqiInterface.java
//定義Java的函數式接口
public interface daqiInterface {
String absMethod();
}
#daqiJava.java
public class daqiJava {
public void setDaqiInterface(daqiInterface listener){
}
}
#daqiKotlin.kt
//調用SAM構造方法
val interfaceObject = daqiInterface {
//返回String類型值
"daqi"
}
//顯示傳遞給接收該函數式接口實例的函數
val daqiJava = daqiJava()
//此處不會報錯
daqiJava.setDaqiInterface(interfaceObject)
對interfaceObject進行類型判斷:
if (interfaceObject is daqiInterface){
println("該對象是daqiInterface實例")
}else{
println("該對象不是daqiInterface實例")
}
??????當單個方法接收多個函數式接口實例時,要么全部顯式調用SAM構造方法菜职,要么全部交給編譯器自行轉換:
#daqiJava.java
public class daqiJava {
public void setDaqiInterface2(daqiInterface listener,Runnable runnable){
}
}
#daqiKotlin.kt
val daqiJava = daqiJava()
//全部交由編譯器自行轉換
daqiJava.setDaqiInterface2( {"daqi"} ){
}
//全部手動顯式SAM轉換
daqiJava.setDaqiInterface2(daqiInterface { "daqi" }, Runnable { })
注意:
- SAM轉換只適用于接口媒惕,不適用于抽象類刊头,即使這些抽象類也只有一個抽象方法。
- SAM轉換 只適用于操作Java類中接收Java函數式接口實例的方法。因為Kotlin具有完整的函數類型贞绵,不需要將函數自動轉換為Kotlin接口的實現。因此严望,需要接收lambda的作為參數的Kotlin函數應該使用函數類型而不是函數式接口村象。
帶接收者的lambda表達式
??????目前講到的lambda都是普通lambda,lambda中還有一種類型:帶接收者的lambda鹅很。
帶接受者的lambda的類型定義:
A.() -> C
表示可以在A類型的接收者對象上調用并返回一個C類型值的函數嘶居。
??????帶接收者的lambda好處是,在lambda函數體可以無需任何額外的限定符的情況下促煮,直接使用接收者對象的成員(屬性或方法)邮屁,亦可使用this訪問接收者對象。
??????似曾相識的擴展函數中菠齿,this關鍵字也執(zhí)行擴展類的實例對象佑吝,而且也可以被省略掉。擴展函數某種意義上就是帶接收者的函數绳匀。
??????擴展函數和帶接收者的lambda極為相似芋忿,雙方都需要一個接收者對象炸客,雙方都可以直接調用該對象的成員。如果將普通lambda當作普通函數的匿名方式來看看待戈钢,那么帶接收者類型的lambda可以當作擴展函數的匿名方式來看待痹仙。
Kotlin的標準庫中就有提供帶接收者的lambda表達式:with和apply
val stringBuilder = StringBuilder()
val result = with(stringBuilder){
append("daqi在努力學習Android")
append("daqi在努力學習Kotlin")
//最后一個表達式作為返回值返回
this.toString()
}
//打印結果便是上面添加的字符串
println(result)
with函數,顯式接收接收者殉了,并將lambda最后一個表達式的返回值作為with函數的返回值返回开仰。
查看with函數的定義:
public inline fun <T, R> with(receiver: T, block: T.() -> R): R {
}
??????其lambda的函數類型表示,參數類型和返回值類型可以為不同值薪铜,也就是說可以返回與接收者類型不一致的值众弓。
??????apply函數幾乎和with函數一模一樣,唯一區(qū)別是apply始終返回接收者對象隔箍。對with的代碼進行重構:
val stringBuilder = StringBuilder().apply {
append("daqi在努力學習Android")
append("daqi在努力學習Kotlin")
}
println(stringBuilder.toString())
查看apply函數的定義:
public inline fun <T> T.apply(block: T.() -> Unit): T {
}
??????函數被聲明為T類型的擴展函數谓娃,并返回T類型的對象。由于其泛型的緣故鞍恢,可以在任何對象上使用apply傻粘。
??????apply函數在創(chuàng)建一個對象并需要對其進行初始化時非常有效。在Java中帮掉,一般借助Builder對象弦悉。
lambda表達式的使用場景
- 場景一:lambda和集合一起使用,是lambda最經典的用途蟆炊』颍可以對集合進行篩選、映射等其他操作涩搓。
val languages = listOf("Java","Kotlin","Python","JavaScript")
languages.filter {
it.contains("Java")
}.forEach{
println(it)
}
- 場景二:替代函數式接口實例
//替代View.OnClickListener接口
mName.setOnClickListener {
}
//替代Runnable接口
mHandler.post {
}
- 場景三:需要接收函數類型變量的函數
//定義函數
fun daqi(string:(Int) -> String){
}
//使用
daqi{
}
有限返回
??????前面說lambda一般是將lambda中最后一個表達式的返回值作為lambda的返回值污秆,這種返回是隱式發(fā)生的,不需要額外的語法昧甘。但當多個lambda嵌套良拼,需要返回外層lambda時,可以使用有限返回充边。
有限返回就是帶標簽的return
??????標簽一般是接收lambda實參的函數名庸推。當需要顯式返回lambda結果時,可以使用有限返回的形式將結果返回浇冰。例子:
val array = listOf("Java","Kotlin")
val buffer = with(StringBuffer()) {
array.forEach { str ->
if (str.equals("Kotlin")){
//返回添加Kotlin字符串的StringBuffer
return@with this.append(str)
}
}
}
println(buffer.toString())
??????lambda表達式內部禁止使用裸return贬媒,因為一個不帶標簽的return語句總是在用fun關鍵字聲明的函數中返回。這意味著lambda表達式中的return將從包含它的函數返回肘习。
fun main(args: Array<String>) {
StringBuffer().apply {
//打印第一個daqi
println("daqi")
return
}
//打印第二個daqi
println("daqi")
}
結果是:第一次打印完后际乘,便退出了main函數。
匿名函數
??????lambda表達式語法缺少指定函數的返回類型的能力漂佩,當需要顯式指定返回類型時脖含,可以使用匿名函數罪塔。匿名函數除了名稱省略,其他和常規(guī)函數聲明一致器赞。
fun(x: Int, y: Int): Int {
return x + y
}
與lambda不同垢袱,匿名函數中的return是從匿名函數中返回。
lambda變量捕捉
??????在Java中港柜,當函數內聲明一個匿名內部類或者lambda時候,匿名內部類能引用這個函數的參數和局部變量咳榜,但這些參數和局部變量必須用final修飾夏醉。Kotlin的lambda一樣也可以訪問函數參數和局部變量,并且不局限于final變量涌韩,甚至能修改非final的局部變量畔柔!Kotlin的lambda表達式是真正意思上的閉包。
fun daqi(func:() -> Unit){
func()
}
fun sum(x:Int,y:Int){
var count = x + y
daqi{
count++
println("$x + $y +1 = $count")
}
}
??????正常情況下臣樱,局部變量的生命周期都會被限制在聲明該變量的函數中靶擦,局部變量在函數被執(zhí)行完后就會被銷毀。但局部變量或參數被lambda捕捉后雇毫,使用該變量的代碼塊可以被存儲并延遲執(zhí)行玄捕。這是為什么呢?
??????當捕捉final變量時棚放,final變量會被拷貝下來與使用該final變量的lambda代碼一起存儲枚粘。而對于非final變量會被封裝在一個final的Ref包裝類實例中,然后和final變量一樣飘蚯,和使用該變量lambda一起存儲馍迄。當需要修改這個非final引用時,通過獲取Ref包裝類實例局骤,進而改變存儲在該包裝類中的布局變量攀圈。所以說lambda還是只能捕捉final變量,只是Kotlin屏蔽了這一層包裝峦甩。
查看源碼:
public static final void sum(final int x, final int y) {
//創(chuàng)建一個IntRef包裝類對象赘来,將變量count存儲進去
final IntRef count = new IntRef();
count.element = x + y;
daqi((Function0)(new Function0() {
public Object invoke() {
this.invoke();
return Unit.INSTANCE;
}
public final void invoke() {
//通過包裝類對象對內部的變量進行讀和修改
int var10001 = count.element++;
String var1 = x + " + " + y + " +1 = " + count.element;
System.out.println(var1);
}
}));
}
注意: 對于lambda修改局部變量,只有在該lambda表達式被執(zhí)行的時候觸發(fā)穴店。
成員引用
??????lambda可以將代碼塊作為參數傳遞給函數撕捍,但當我需要傳遞的代碼已經被定義為函數時,該怎么辦泣洞?難不成我寫一個調用該函數的lambda忧风?Kotlin和Java8允許你使用成員引用將函數轉換成一個值,然后傳遞它球凰。
成員引用用來創(chuàng)建一個調用單個方法或者訪問單個屬性的函數值狮腿。
data class Person(val age:Int,val name:String)
fun daqi(){
val persons = listOf(Person(17,"daqi"),Person(20,"Bob"))
persons.maxBy({person -> person.age })
}
??????Kotlin中腿宰,當你聲明屬性的時候,也就聲明了對應的訪問器(即get和set)缘厢。此時Person類中已存在age屬性的訪問器方法吃度,但我們在調用訪問器時,還在外面嵌套了一層lambda贴硫。使用成員引用進行優(yōu)化:
data class Person(val age:Int,val name:String)
fun daqi(){
val persons = listOf(Person(17,"daqi"),Person(20,"Bob"))
persons.maxBy(Person::age)
}
成員引用由類椿每、雙冒號、成員三個部分組成:
頂層函數和擴展函數都可以使用成員引用來表示:
//頂層函數
fun daqi(){
}
//擴展函數
fun Person.getPersonAge(){
}
fun main(args: Array<String>) {
//頂層函數的成員引用(不附屬于任何一個類英遭,類省略)
run(::daqi)
//擴展函數的成員引用
Person(17,"daqi").run(Person::getPersonAge)
}
還可以對構造函數使用成員引用來表示:
val createPerson = ::Person
val person = createPerson(17,"daqi")
Kotlin1.1后间护,成員引用語法支持捕捉特定實例對象上的方法引用:
val personAge = Person(17,"name")::age
lambda的性能優(yōu)化
??????自Kotlin1.0起,每一個lambda表達式都會被編譯成一個匿名類挖诸,帶來額外的開銷汁尺。可以使用內聯函數來優(yōu)化lambda帶來的額外消耗多律。
??????所謂的內聯函數痴突,就是使用inline修飾的函數。在函數被使用的地方編譯器并不會生成函數調用的代碼狼荞,而是將函數實現的真實代碼替換每一次的函數調用辽装。Kotlin中大多數的庫函數都標記成了inline。
參考資料:
- 《Kotlin實戰(zhàn)》
- Kotlin官網