前序
在Kotlin中,函式作為一等公民存在,函式可以像值一樣被傳遞。lambda就是將一小段程式碼封裝成匿名函式,以引數值的方式傳遞到函式中,供函式使用。
初識lambda
在Java8之前,當外部需要設定一個類中某種事件的處理邏輯時,往往需要定義一個介面(類),並建立其匿名例項作為引數,具體的處理邏輯存放到某個對應的方法中來實現:
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放在括號外面,這時使用常規傳參語法來實現是最好的選擇。
SAM 轉換
回看剛開始的setOnClickListener()方法,那接收的引數是一個介面例項,不是函式型別呀!怎麼就可以傳lambda了呢?先了解一個概念:函式式介面:
函式式介面就是隻定義一個抽象方法的介面
SAM轉換就是將lambda顯示轉換為函式式介面例項,但要求Kotlin的函式型別和該SAM(單一抽象方法)的函式型別一致。SAM轉換一般都是自動發生的。
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關鍵字也執行擴充套件類的例項物件,而且也可以被省略掉。擴充套件函式某種意義上就是帶接收者的函式。
擴充套件函式和帶接收者的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函式一模一樣,唯一區別是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函式在建立一個物件並需要對其進行初始化時非常有效。在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的返回值,這種返回是隱式發生的,不需要額外的語法。但當多個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表示式語法缺少指定函式的返回型別的能力,當需要顯式指定返回型別時,可以使用匿名函式。匿名函式除了名稱省略,其他和常規函式宣告一致。
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")
}
}
複製程式碼
正常情況下,區域性變數的生命週期都會被限制在宣告該變數的函式中,區域性變數在函式被執行完後就會被銷燬。但區域性變數或引數被lambda捕捉後,使用該變數的程式碼塊可以被儲存並延遲執行。這是為什麼呢?
當捕捉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) {
//建立一個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表示式被執行的時候觸發。
成員引用
lambda可以將程式碼塊作為引數傳遞給函式,但當我需要傳遞的程式碼已經被定義為函式時,該怎麼辦?難不成我寫一個呼叫該函式的lambda?Kotlin和Java8允許你使用成員引用將函式轉換成一個值,然後傳遞它。
成員引用用來建立一個呼叫單個方法或者訪問單個屬性的函式值。
複製程式碼
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。使用成員引用進行優化:
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的效能優化
自Kotlin1.0起,每一個lambda表示式都會被編譯成一個匿名類,帶來額外的開銷。可以使用行內函數來優化lambda帶來的額外消耗。
所謂的行內函數,就是使用inline修飾的函式。在函式被使用的地方編譯器並不會生成函式呼叫的程式碼,而是將函式實現的真實程式碼替換每一次的函式呼叫。Kotlin中大多數的庫函式都標記成了inline。
參考資料:
- 《Kotlin實戰》
- Kotlin官網