淺談Kotlin語法篇之Lambda表達式徹底解析(六)

簡述: 今天帶來的Kotlin淺談系列的第六彈, 一塊兒來聊下Kotlin中的lambda表達式。lambda表達式應該都不陌生,在Java8中引入的一個很重要的特性,將開發者從原來繁瑣的語法中解放出來,但是很遺憾的是隻有Java8版本才能使用。而Kotlin則彌補了這一問題,Kotlin中的lambda表達式與Java混合編程能夠支持Java8如下的版本。那咱們帶着如下幾個問題一塊兒來看下Kotlin中lambda表達式。java

  • 一、爲何要使用Kotlin的lambda表達式(why)?
  • 二、如何去使用Kotlin的lambda表達式(how)?
  • 三、Kotlin的lambda表達式通常用在哪(where)?
  • 四、Kotlin的lambda表達式的做用域變量和變量捕獲
  • 五、Kotlin的lambda表達式的成員引用

1、爲何要使用Kotlin的lambda表達式?

針對以上爲何使用Kotlin中的lambda表達式的問題,我以爲有三點主要的緣由。android

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

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

一、lambda表達式分類

在Kotlin實際上能夠把Lambda表達式分爲兩個大類,一個是普通的lambda表達式,另外一個則是帶接收者的lambda表達式(功能很強大,以後會有專門分析的博客)。這兩種lambda在使用和使用場景也是有很大的不一樣. 先看下如下兩種lambda表達式的類型聲明:編程

針對帶接收者的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表達式的分類,你是否是想到以前的擴展函數了,有沒有想起以前這張圖?bash

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

二、lambda基本語法

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

含有實際參數jvm

含有函數體(儘管函數體爲空,也得聲明出來)ide

以上內部必須被包含在花括號內部函數式編程

以上是lambda表達式最標準的形式,可能這種標準形式在之後的開發中可能見到比較少,更可能是更加的簡化形式,下面就是會介紹Lambda表達式簡化規則

三、lambda語法簡化轉換

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

注意:語法簡化是把雙刃劍,簡化當然不錯,使用簡單方便,可是不能濫用,也須要考慮到代碼的可讀性.上圖中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" }))
}
複製代碼

四、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))
}
複製代碼

將函數體內的兩個表達式互換位置後

package com.mikyou.kotlin.lambda

fun main(args: Array<String>) {

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

    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);
    }
}

複製代碼

五、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類型,而後再用相似方法往內部拆分。

六、使用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)
}
複製代碼

3、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)
				}
			})
}

複製代碼

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

一、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表達式是怎麼作到這一點的呢?請接着看

二、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();
      }

   }
}

複製代碼

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

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

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

一、爲何要使用成員引用

咱們知道在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表達式類型一致
}
複製代碼

二、成員引用的基本語法

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

三、成員引用的使用場景

  • 成員引用最多見的使用方式就是類名+雙冒號+成員(屬性或函數)
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的基礎知識就基本淺談完畢了,下一篇會從Lambda實質原理和字節碼方面分析,以及Lambda表達式使用時性能優化。

歡迎關注Kotlin開發者聯盟,這裏有最新Kotlin技術文章,每週會不按期翻譯一篇Kotlin國外技術文章。若是你也喜歡Kotlin,歡迎加入咱們~~~

相關文章
相關標籤/搜索