Kotlin知識概括(五) —— Lambda

前序

      在Kotlin中,函數做爲一等公民存在,函數能夠像值同樣被傳遞。lambda就是將一小段代碼封裝成匿名函數,以參數值的方式傳遞到函數中,供函數使用。java

初識lambda

      在Java8以前,當外部須要設置一個類中某種事件的處理邏輯時,每每須要定義一個接口(類),並建立其匿名實例做爲參數,具體的處理邏輯存放到某個對應的方法中來實現:android

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的返回值類型。如今一個函數必需的參數列表、函數體和返回類型都一一找出來了。bash

函數類型

      都說能夠將函數做爲變量值傳遞,那該變量的類型如何定義呢?函數變量的類型統稱函數類型,所謂函數類型就是聲明該函數的參數類型列表和函數返回值類型。閉包

先看個簡單的函數類型:app

() -> Unit
複製代碼

      函數類型和lambda同樣,使用 -> 做分隔符,但函數類型是將參數類型列表和返回值類型分開,全部函數類型都有一個圓括號括起來的參數類型列表和返回值類型。ide

一些相對簡單的函數類型:函數

//無參、無返回值的函數類型(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。post

      當顯示聲明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。

參考資料:

android Kotlin系列:

Kotlin知識概括(一) —— 基礎語法

Kotlin知識概括(二) —— 讓函數更好調用

Kotlin知識概括(三) —— 頂層成員與擴展

Kotlin知識概括(四) —— 接口和類

Kotlin知識概括(五) —— Lambda

Kotlin知識概括(六) —— 類型系統

Kotlin知識概括(七) —— 集合

Kotlin知識概括(八) —— 序列

Kotlin知識概括(九) —— 約定

Kotlin知識概括(十) —— 委託

Kotlin知識概括(十一) —— 高階函數

Kotlin知識概括(十二) —— 泛型

Kotlin知識概括(十三) —— 註解

Kotlin知識概括(十四) —— 反射

相關文章
相關標籤/搜索