Kotlin 語法篇之Lambda表達式完全解析


Lambda表達式應該都不陌生,在Java8中引入的一個很重要的特性,將開發者從原來繁瑣的語法中解放出來,可是很遺憾的是隻有Java8版本才能使用。而Kotlin則彌補了這一問題,Kotlin中的lambda表達式與Java混合編程可以支持Java8以下的版本。那我們帶着以下幾個問題一起來看下Kotlin中lambda表達式。

一、爲什麼要使用Kotlin的lambda表達式?

針對以上爲什麼使用Kotlin中的lambda表達式的問題,我覺得有三點主要的原因。

  • 1、Kotlin的lambda表達式以更加簡潔易懂的語法實現功能,使開發者從原有冗餘囉嗦的語法聲明解放出來。可以使用函數式編程中的過濾、映射、轉換等操作符處理集合數據,從而使你的代碼更加接近函數式編程的風格。
  • 2、Java8以下的版本不支持Lambda表達式,而Kotlin則兼容與Java8以下版本有很好互操作性,非常適合Java8以下版本與Kotlin混合開發的模式。解決了Java8以下版本不能使用lambda表達式瓶頸。
  • 3、在Java8版本中使用Lambda表達式是有些限制的,它不是真正意義上支持閉包,而Kotlin中lambda纔是真正意義的支持閉包實現。(關於這個問題爲什麼下面會有闡述)

二、Kotlin的lambda表達式基本語法

1、lambda表達式分類

在Kotlin實際上可以把Lambda表達式分爲兩個大類,一個是普通的lambda表達式,另一個則是帶接收者的lambda表達式(功能很強大,之後會有專門分析的博客)。這兩種lambda在使用和使用場景也是有很大的不同. 先看下以下兩種lambda表達式的類型聲明:
image
針對帶接收者的Lambda表達式在Kotlin中標準庫函數中也是非常常見的比如with,apply標準函數的聲明。

	@kotlin.internal.InlineOnly
	public inline fun <T, R> with(receiver: T, block: T.() -> R): R {
	    contract {
	        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
	    }
	    return receiver.block()
	}
	
	@kotlin.internal.InlineOnly
	public inline fun <T> T.apply(block: T.() -> Unit): T {
	    contract {
	        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
	    }
	    block()
	    return this
	}

是不是和我們之前博客說普通函數和擴展函數類似。普通的Lambda表達式類似對應普通函數的聲明,而帶接收者的lambda表達式則類似對應擴展函數。擴展函數就是這種聲明接收者類型,然後使用接收者對象調用直接類似成員函數調用,實際內部是通過這個接收者對象實例直接訪問它的方法和屬性。

2、lambda基本語法

lambda的標準形式基本聲明滿足三個條件:

  • 含有實際參數
  • 含有函數體(儘管函數體爲空,也得聲明出來)
  • 以上內部必須被包含在花括號內部
    在這裏插入圖片描述
    以上是lambda表達式最標準的形式,可能這種標準形式在以後的開發中可能見到比較少,更多是更加的簡化形式,下面就是會介紹Lambda表達式簡化規則

3、lambda語法簡化轉換

以後開發中我們更多的是使用簡化版本的lambda表達式,因爲看到標準的lambda表達式形式還是有些囉嗦,比如實參類型就可以省略,因爲Kotlin這門語言支持根據上下文環境智能推導出類型,所以可以省略,摒棄囉嗦的語法,下面是lambda簡化規則。

作者:mikyou鏈接:https://www.jianshu.com/p/d980a89cc187來源:簡書著作權歸作者所有。商業轉載請聯繫作者獲得授權,非商業轉載請註明出處。
注意:語法簡化是把雙刃劍,簡化固然不錯,使用簡單方便,但是不能濫用,也需要考慮到代碼的可讀性.上圖中Lambda化簡成的最簡單形式用it這種,一般在多個Lambda嵌套的時候不建議使用,嚴重造成代碼可讀性,到最後估計連開發者都不知道it指代什麼了。比如以下代碼:

這是Kotlin庫中的joinToString擴展函數,最後一個參數是一個接收一個集合元素類型T的參數返回一個CharSequence類型的lambda表達式。

//joinToString內部聲明
public fun <T> Iterable<T>.joinToString(separator: CharSequence = ", ", prefix: CharSequence = "", postfix: CharSequence = "", limit: Int = -1, truncated: CharSequence = "...", transform: ((T) -> CharSequence)? = null): String {
    return joinTo(StringBuilder(), separator, prefix, postfix, limit, truncated, transform).toString()
}


fun main(args: Array<String>) {
    val num = listOf(1, 2, 3)
    println(num.joinToString(separator = ",", prefix = "<", postfix = ">") {
        return@joinToString "index$it"
    })
}

我們可以看到joinToString的調用地方是使用了lambda表達式作爲參數的簡化形式,將它從圓括號中提出來了。這個確實給調用帶來一點小疑惑,因爲並沒有顯示錶明lambda表達式應用到哪裏,所以不熟悉內部實現的開發者很難理解。對於這種問題,Kotlin實際上給我們提供解決辦法,也就是我們之前博客提到過的命名參數。
使用命名參數後的代碼

//joinToString內部聲明
public fun <T> Iterable<T>.joinToString(separator: CharSequence = ", ", prefix: CharSequence = "", postfix: CharSequence = "", limit: Int = -1, truncated: CharSequence = "...", transform: ((T) -> CharSequence)? = null): String {
    return joinTo(StringBuilder(), separator, prefix, postfix, limit, truncated, transform).toString()
}
fun main(args: Array<String>) {
    val num = listOf(1, 2, 3)
    println(num.joinToString(separator = ",", prefix = "<", postfix = ">", transform = { "index$it" }))
}

4、lambda表達式的返回值

lambda表達式返回值總是返回函數體內部最後一行表達式的值

package com.mikyou.kotlin.lambda

fun main(args: Array<String>) {

    val isOddNumber = { number: Int ->
        println("number is $number")
        number % 2 == 1
    }

    println(isOddNumber.invoke(100))
}

通過上面例子可以看出lambda表達式是返回函數體內最後一行表達式的值,由於println函數沒有返回值,所以默認打印出來的是Unit類型,那它內部原理是什麼呢?實際上是通過最後一行表達式返回值類型作爲了invoke函數的返回值的類型,我們可以對比上述兩種寫法的反編譯成java的代碼:

//互換位置之前的反編譯代碼
package com.mikyou.kotlin.lambda;

import kotlin.jvm.internal.Lambda;

@kotlin.Metadata(mv = {1, 1, 10}, bv = {1, 0, 2}, k = 3, d1 = {"\000\016\n\000\n\002\020\013\n\000\n\002\020\b\n\000\020\000\032\0020\0012\006\020\002\032\0020\003H\n¢\006\002\b\004"}, d2 = {"<anonymous>", "", "number", "", "invoke"})
final class LambdaReturnValueKt$main$isOddNumber$1 extends Lambda implements kotlin.jvm.functions.Function1<Integer, Boolean> {
    public final boolean invoke(int number) {//此時invoke函數返回值的類型是boolean,對應了Kotlin中的Boolean
        String str = "number is " + number;
        System.out.println(str);
        return number % 2 == 1;
    }

    public static final 1INSTANCE =new 1();

    LambdaReturnValueKt$main$isOddNumber$1() {
        super(1);
    }
}


//互換位置之後的反編譯代碼
package com.mikyou.kotlin.lambda;

import kotlin.jvm.internal.Lambda;

@kotlin.Metadata(mv = {1, 1, 10}, bv = {1, 0, 2}, k = 3, d1 = {"\000\016\n\000\n\002\020\002\n\000\n\002\020\b\n\000\020\000\032\0020\0012\006\020\002\032\0020\003H\n¢\006\002\b\004"}, d2 = {"<anonymous>", "", "number", "", "invoke"})
final class LambdaReturnValueKt$main$isOddNumber$1 extends Lambda implements kotlin.jvm.functions.Function1<Integer, kotlin.Unit> {
    public final void invoke(int number) {//此時invoke函數返回值的類型是void,對應了Kotlin中的Unit
        if (number % 2 != 1) {
        }
        String str = "number is " + number;
        System.out.println(str);
    }

    public static final 1INSTANCE =new 1();

    LambdaReturnValueKt$main$isOddNumber$1() {
        super(1);
    }
}

5、lambda表達式類型

Kotlin中提供了簡潔的語法去定義函數的類型.

() -> Unit//表示無參數無返回值的Lambda表達式類型

(T) -> Unit//表示接收一個T類型參數,無返回值的Lambda表達式類型

(T) -> R//表示接收一個T類型參數,返回一個R類型值的Lambda表達式類型

(T, P) -> R//表示接收一個T類型和P類型的參數,返回一個R類型值的Lambda表達式類型

(T, (P,Q) -> S) -> R//表示接收一個T類型參數和一個接收P、Q類型兩個參數並返回一個S類型的值的Lambda表達式類型參數,返回一個R類型值的Lambda表達式類型

上面幾種類型前面幾種應該好理解,估計有點難度是最後一種,最後一種實際上已經屬於高階函數的範疇。不過這裏說下個人看這種類型的一個方法有點像剝洋蔥一層一層往內層拆分,就是由外往裏看,然後做拆分,對於本身是一個Lambda表達式類型的,先暫時看做一個整體,這樣就可以確定最外層的Lambda類型,然後再用類似方法往內部拆分。

6、使用typealias關鍵字給Lambda類型命名

我們試想一個場景就是可能會用到多個lambda表達式,但是這些lambda表達式的類型很多相同,我們就很容易把所有相同一大串的Lambda類型重複聲明或者你的lambda類型聲明太長不利於閱讀。實際上不需要,對於Kotlin這門反對一切囉嗦語法的語言來說,它都給你提供一系列的解決辦法,讓你簡化代碼的同時又不降低代碼的可讀性。

fun main(args: Array<String>) {
    val oddNum:  (Int) -> Unit = {
        if (it % 2 == 1) {
            println(it)
        } else {
            println("is not a odd num")
        }
    }

    val evenNum:  (Int) -> Unit = {
        if (it % 2 == 0) {
            println(it)
        } else {
            println("is not a even num")
        }
    }

    oddNum.invoke(100)
    evenNum.invoke(100)
}

使用typealias關鍵字聲明(Int) -> Unit類型

package com.mikyou.kotlin.lambda

typealias NumPrint = (Int) -> Unit//注意:聲明的位置在函數外部,package內部

fun main(args: Array<String>) {
    val oddNum: NumPrint = {
        if (it % 2 == 1) {
            println(it)
        } else {
            println("is not a odd num")
        }
    }

    val evenNum: NumPrint = {
        if (it % 2 == 0) {
            println(it)
        } else {
            println("is not a even num")
        }
    }

    oddNum.invoke(100)
    evenNum.invoke(100)
}

三、Kotlin的lambda表達式經常使用的場景

  • 場景一: lambda表達式與集合一起使用,是最常見的場景,可以各種篩選、映射、變換操作符和對集合數據進行各種操作,非常靈活,相信使用過RxJava中的開發者已經體會到這種快感,沒錯Kotlin在語言層面,無需增加額外庫,就給你提供了支持函數式編程API。
package com.mikyou.kotlin.lambda

fun main(args: Array<String>) {
    val nameList = listOf("Kotlin", "Java", "Python", "JavaScript", "Scala", "C", "C++", "Go", "Swift")
    nameList.filter {
        it.startsWith("K")
    }.map {
        "$it is a very good language"
    }.forEach {
        println(it)
    }

}
  • 場景二: 替代原有匿名內部類,但是需要注意一點就是隻能替代含有單抽象方法的類。
    findViewById(R.id.submit).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                ...
            }
        });

用kotlin lambda實現

findViewById(R.id.submit).setOnClickListener{
    ...
}
  • 場景三: 定義Kotlin擴展函數或者說需要把某個操作或函數當做值傳入的某個函數的時候。
fun Context.showDialog(content: String = "", negativeText: String = "取消", positiveText: String = "確定", isCancelable: Boolean = false, negativeAction: (() -> Unit)? = null, positiveAction: (() -> Unit)? = null) {
    AlertDialog.build(this)
            .setMessage(content)
            .setNegativeButton(negativeText) { _, _ ->
                negativeAction?.invoke()
            }
            .setPositiveButton(positiveText) { _, _ ->
                positiveAction?.invoke()
            }
            .setCancelable(isCancelable)
            .create()
            .show()
}

fun Context.toggleSpFalse(key: String, func: () -> Unit) {
    if (!getSpBoolean(key)) {
        saveSpBoolean(key, true)
        func()
    }
}

fun <T : Any> Observable<T>.subscribeKt(success: ((successData: T) -> Unit)? = null, failure: ((failureError: RespException?) -> Unit)? = null): Subscription? {
    return transformThread()
            .subscribe(object : SBRespHandler<T>() {
                override fun onSuccess(data: T) {
                    success?.invoke(data)
                }

                override fun onFailure(e: RespException?) {
                    failure?.invoke(e)
                }
            })
}

四、Kotlin的lambda表達式的作用域中訪問變量和變量捕獲

1、Kotlin和Java內部類或lambda訪問局部變量的區別

  • 在Java中在函數內部定義一個匿名內部類或者lambda,內部類訪問的函數局部變量必須需要final修飾,也就意味着在內部類內部或者lambda表達式的內部是無法去修改函數局部變量的值。可以看一個很簡單的Android事件點擊的例子
public class DemoActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_demo);
        final int count = 0;//需要使用final修飾
        findViewById(R.id.btn_click).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                System.out.println(count);//在匿名OnClickListener類內部訪問count必須要是final修飾
            }
        });
    }
}
  • 在Kotlin中在函數內部定義lambda或者內部類,既可以訪問final修飾的變量,也可以訪問非final修飾的變量,也就意味着在Lambda的內部是可以直接修改函數局部變量的值。以上例子Kotlin實現

訪問final修飾的變量

class Demo2Activity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_demo2)
        val count = 0//聲明final
        btn_click.setOnClickListener {
            println(count)//訪問final修飾的變量這個是和Java是保持一致的。
        }
    }
}

訪問非final修飾的變量,並修改它的值

class Demo2Activity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_demo2)
        var count = 0//聲明非final類型
        btn_click.setOnClickListener {
            println(count++)//直接訪問和修改非final類型的變量
        }
    }
}

通過以上對比會發現Kotlin中使用lambda會比Java中使用lambda更靈活,訪問受到限制更少,這也就回答本博客最開始說的一句話,Kotlin中的lambda表達式是真正意義上的支持閉包,而Java中的lambda則不是。Kotlin中的lambda表達式是怎麼做到這一點的呢?請接着看

2、Kotlin中lambda表達式的變量捕獲及其原理

  • 什麼是變量捕獲?

通過上述例子,我們知道在Kotlin中既能訪問final的變量也能訪問或修改非final的變量。原理是怎樣的呢?在此之前先拋出一個高大上的概念叫做lambdab表達式的變量捕獲。實際上就是lambda表達式在其函數體內可以訪問外部的變量,我們就稱這些外部變量被lambda表達式給捕獲了。有了這個概念我們可以把上面的結論變得高大上一些:

第一在Java中lambda表達式只能捕獲final修飾的變量

第二在Kotlin中lambda表達式既能捕獲final修飾的變量也能訪問和修改非final的變量

  • 變量捕獲實現的原理

我們都知道函數的局部變量生命週期是屬於這個函數的,當函數執行完畢,局部變量也就是銷燬了,但是如果這個局部變量被lambda捕獲了,那麼使用這個局部變量的代碼將會被存儲起來等待稍後再次執行,也就是被捕獲的局部變量是可以延遲生命週期的,針對lambda表達式捕獲final修飾的局部變量原理是局部變量的值和使用這個值的lambda代碼會被一起存儲起來;而針對於捕獲非final修飾的局部變量原理是非final局部變量會被一個特殊包裝器類包裝起來,這樣就可以通過包裝器類實例去修改這個非final的變量,那麼這個包裝器類實例引用是final的會和lambda代碼一起存儲。

以上第二條結論在Kotlin的語法層面來說是正確的,但是從真正的原理上來說是錯誤的,只不過是Kotlin在語法層面把這個屏蔽了而已,實質的原理lambda表達式還是隻能捕獲final修飾變量,而爲什麼kotlin卻能做到修改非final的變量的值,實際上kotlin在語法層面做了一個橋接包裝,它把所謂的非final的變量用一個Ref包裝類包裝起來,然後外部保留着Ref包裝器的引用是final的,然後lambda會和這個final包裝器的引用一起存儲,隨後在lambda內部修改變量的值實際上是通過這個final的包裝器引用去修改的。

最後通過查看Kotlin修改非final局部變量的反編譯成的Java代碼就是一目瞭然了

class Demo2Activity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_demo2)
        var count = 0//聲明非final類型
        btn_click.setOnClickListener {
            println(count++)//直接訪問和修改非final類型的變量
        }
    }
}
@Metadata(
   mv = {1, 1, 9},
   bv = {1, 0, 2},
   k = 1,
   d1 = {"\u0000\u0018\n\u0002\u0018\u0002\n\u0002\u0018\u0002\n\u0002\b\u0002\n\u0002\u0010\u0002\n\u0000\n\u0002\u0018\u0002\n\u0000\u0018\u00002\u00020\u0001B\u0005¢\u0006\u0002\u0010\u0002J\u0012\u0010\u0003\u001a\u00020\u00042\b\u0010\u0005\u001a\u0004\u0018\u00010\u0006H\u0014¨\u0006\u0007"},
   d2 = {"Lcom/shanbay/prettyui/prettyui/Demo2Activity;", "Landroid/support/v7/app/AppCompatActivity;", "()V", "onCreate", "", "savedInstanceState", "Landroid/os/Bundle;", "production sources for module app"}
)
public final class Demo2Activity extends AppCompatActivity {
   private HashMap _$_findViewCache;

   protected void onCreate(@Nullable Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      this.setContentView(2131361820);
      final IntRef count = new IntRef();//IntRef特殊的包裝器類的類型,final修飾的IntRef的count引用
      count.element = 0;//包裝器內部的非final變量element
      ((Button)this._$_findCachedViewById(id.btn_click)).setOnClickListener((OnClickListener)(new OnClickListener() {
         public final void onClick(View it) {
            int var2 = count.element++;//直接是通過IntRef的引用直接修改內部的非final變量的值,來達到語法層面的lambda直接修改非final局部變量的值
            System.out.println(var2);
         }
      }));
   }

   public View _$_findCachedViewById(int var1) {
      if(this._$_findViewCache == null) {
         this._$_findViewCache = new HashMap();
      }

      View var2 = (View)this._$_findViewCache.get(Integer.valueOf(var1));
      if(var2 == null) {
         var2 = this.findViewById(var1);
         this._$_findViewCache.put(Integer.valueOf(var1), var2);
      }

      return var2;
   }

   public void _$_clearFindViewByIdCache() {
      if(this._$_findViewCache != null) {
         this._$_findViewCache.clear();
      }

   }
}

3、Kotlin中lambda表達式變量捕獲注意事項

注意: 對於Lambda表達式內部修改局部變量的值,只會在這個Lambda表達式被執行的時候觸發。

五、Kotlin的lambda表達式的成員引用

1、爲什麼要使用成員引用

我們知道在Lambda表達式可以直接把一個代碼塊作爲一個參數傳遞給函數,但是有沒有遇到過這樣一個場景就是我要傳遞過去的代碼塊,已經是作爲了一個命名函數存在了,此時你還需要重複寫一個代碼塊傳遞過去嗎?肯定不是,Kotlin拒絕囉嗦重複的代碼。所以只需要成員引用替代即可。

fun main(args: Array<String>) {
    val persons = listOf(Person(name = "Alice", age = 18), Person(name = "Mikyou", age = 20), Person(name = "Bob", age = 16))
    println(persons.maxBy({ p: Person -> p.age }))
}

可以替代爲

fun main(args: Array<String>) {
    val persons = listOf(Person(name = "Alice", age = 18), Person(name = "Mikyou", age = 20), Person(name = "Bob", age = 16))
    println(persons.maxBy(Person::age))//成員引用的類型和maxBy傳入的lambda表達式類型一致
}

2、成員引用的基本語法

成員引用由類、雙冒號、成員三個部分組成

3、成員引用的使用場景

  • 成員引用最常見的使用方式就是類名+雙冒號+成員(屬性或函數)
fun main(args: Array<String>) {
    val persons = listOf(Person(name = "Alice", age = 18), Person(name = "Mikyou", age = 20), Person(name = "Bob", age = 16))
    println(persons.maxBy(Person::age))//成員引用的類型和maxBy傳入的lambda表達式類型一致
}
  • 省略類名直接引用頂層函數(之前博客有專門分析)
package com.mikyou.kotlin.lambda

fun salute() = print("salute")

fun main(args: Array<String>) {
    run(::salute)
}
  • 成員引用用於擴展函數
fun Person.isChild() = age < 18

fun main(args: Array<String>){
    val isChild = Person::isChild
    println(isChild)
}

到這裏有關Kotlin lambda的基礎知識就基本淺談完畢了。

更多精彩內容請掃碼關注:
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章