Kotlin 中的 run、let、with、apply、also、takeIf、takeUnless 語法糖使用和原理分析

在 Kotlin 有一些能夠簡化代碼的語法糖,好比 run、let、with、apply、also、takeIf、takeUnless 等。android

再不明白這些語法糖的狀況下去看 Kotlin 代碼就會一臉懵逼,可當明白以後就會以爲原來能夠這樣簡化。編程

帶接收者的函數字面值

使用這些語法糖以前回顧一下 Kotlin 的函數式編程,在分析 Kotlin 使用 Anko 構建佈局 文章中有提到 帶接收者的函數字面值微信

它的形式是這樣的:app

// 定義一個類
class ReceiveObject 
// 定義一個函數
fun exec(invoke: ReceiveObject.()-> Int){}

在 Kotlin 中,函數也能夠當作變量傳參,例如:less

fun funAsArg(args:()->Int){}
// 調用
funAsArg { 2 }

args 是變量名,它的類型就是函數,函數形式在變量名後面約定:()->Int,函數沒有參數,可是會返回一個 Int 類型的值。函數式編程

而帶接收者的函數字面值,就是在做爲傳入參數的函數變量的具體函數形式的參數前面多了接收者對象,簡單說就是在 ()前面多了一個點和一個對象,成了以下的形式:函數

fun exec(invoke: ReceiveObject.()-> Int){}

就是這多了的一個點和一個對象,讓它有了不同的功能。佈局

簡單的說,invoke 變量是一個函數做爲變量,須要傳遞一個具體函數實現做爲形參給 invoke,那麼在具體函數實現裏面就能夠調用接收者對象 ReceiveObject 的相關方法,以下:post

// 接收者對象,有個 show 方法
    class ReceiveObject{
        fun show(){
            println("call")
        }
    }
    // 具體函數實現
    val invoke: ReceiveObject.() -> Int = {
        this.show() // 用 this 指代 接收者對象 ReceiveObject
        2
    }
    exec(invoke)

如上,在 invoke 方法裏面使用 this 指代 ReceiveObject 對象,能夠調用它的方法。ui

而 invoke 變量是做爲參數傳遞給 exec 函數的,若是 exec 函數爲空,那麼 inkoke 具體實現的 show 方法也不會被調用的,在 exec 中調用 invoke 的方法以下:

fun exec(invoke: ReceiveObject.() -> Int){
    val receObj = ReceiveObject()
    // 兩種調用形式
    // 相似於 ReceiceObject 拓展函數同樣的調用
    receObj.invoke() 
    // 把 ReceiceObject 做爲參數傳遞給 invoke 調用
    invoke(receObj)
}

在 exec 的具體調用中,咱們須要構造一個 ReceiveObject 對象實例,否則怎麼去調用它的 show 方法呢。

在上面的例子中,還須要構造一個指定的接收者對象實例才能完成 invoke 的調用,而 Kotlin 的語法糖中還有一種叫作 拓展函數

拓展函數

拓展函數至關於給某個類添加函數,但這個函數並不屬於這個類的函數,和 static 方法是兩碼事。

fun Context.showToast(msg: String) {
        Toast.makeText(this, msg, Toast.LENGTH_SHORT).show()
    }

在拓展函數中,使用 this 指代被拓展的類實例,上面代碼中 this 指代就是 Context 。

有了 拓展函數和帶接收者的函數字面值,就能夠實現文章標題提到的那些語法糖了。

例如,針對 ReceiveObject 對象添加它的拓展函數,拓展函數的參數又是一個函數,函數是帶接收者的函數字面值,這個接收者對象就是 ReceiveObject 對象它自己,這樣調用 invoke 方法就不用再構造 ReceiveObject 對象了。

fun ReceiveObject.exec(invoke: ReceiveObject.() -> Int){
    invoke()
}

語法糖

下面介紹的語法糖都是位於 Kotlin Standard.kt 文件中的。

run 語法糖

run 的語法糖有兩種:

public inline fun <R> run(block: () -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block()
}

這種語法糖傳遞的參數就僅僅是一個函數,不是帶接收者對象的函數字面值,它的返回結果就是 block 函數調用後的結果。

調用示例:

var result = kotlin.run { 
            "value"
        }

相對於給 arg 變量賦值爲 value 字符串。

run 的另外一種語法糖:

public inline fun <T, R> T.run(block: T.() -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block()
}

首先,這個語法糖是一個拓展函數,並且用到了泛型 <T,R>,T 類型的拓展函數,返回的是 R 類型,T 和 R 能夠相同。

其次,傳遞的參數是帶接收者對象的函數字面值,也就是說能夠在 block 函數裏面調用 T 的相關方法,經過 this 來指代 T ,在 run 方法內部就是調用了 block 方法,返回 block 函數調用後的結果。

調用示例:

val result = "a".run {
                this.plus("b")
            }

Contracts DSL

在 run 的語法糖裏面還出現了以下一段代碼:

contract {
      callsInPlace(block, InvocationKind.EXACTLY_ONCE)
   }

Google 了一番以後

得出原來這是 Kotlin 1.2.x 版本中出現的,但實際並無用,是 Kotlin 後續發展用來解決以下代碼問題的:

if (!x.isNullOrEmpty()) { 
  // we know that 'x != null' here 
  println(x.length)
}

假設 x 是能夠爲 null 的,通過 isNullOrEmpty 函數判斷以後,再執行 println 函數,那麼它確定就不是 null 了,就不須要再加兩個 !! 來表示 x 不爲 null 了,而如今的狀況是要添加 !!

從 Google 來的信息得知, contract 這段代碼就是爲了這樣的問題的。

因爲語法糖都有那樣一段代碼,因此就先把它們去掉了。

let 語法糖

public inline fun <T, R> T.let(block: (T) -> R): R {
    return block(this)
}

let 語法糖傳遞的參數是一個函數,不是帶接收者的函數字面值,但 block 函數的參數就是 T 類型,因此能夠在 block 裏面調用 T 類型的方法,但不能經過 this 來指代 T 了,經過 it 來指代 T 類型。

調用示例:

val result = "a".let {
                it.plus("b")
            }

with 語法糖

public inline fun <T, R> with(receiver: T, block: T.() -> R): R {
    return receiver.block()
}

with 語法糖再也不是一個拓展函數了,而是須要在語法糖的第一個參數裏面傳入接收者對象的實例,第二個參數就是帶接收者的函數字面值實例,返回的也是 block 調用的結果,這一點和 run 語法糖相似。

調用示例:

val result = with("a") {
                this.plus("b")
            }

apply 語法糖

public inline fun <T> T.apply(block: T.() -> Unit): T {
    block()
    return this
}

apply 語法糖和 run 語法糖都相似,只不過它返回的不是 block 函數調用的結果,而是返回調用者自己,返回 T 類型。

also 語法糖

public inline fun <T> T.also(block: (T) -> Unit): T {
    block(this)
    return this
}

also 語法糖和 let 語法糖有點相似,只不過返回的結果不是 block 調用結果,而是返回它自己,返回 T 類型。

調用示例:

var result = "a".also {
                it.plus("b")
            }

takeIf 語法糖

public inline fun <T> T.takeIf(predicate: (T) -> Boolean): T? {
    return if (predicate(this)) this else null
}

takeIf 語法糖會調用 predicate 函數進行判斷,若是爲 true 就返回它自己,不然返回 null 。

takeUnless 語法糖

public inline fun <T> T.takeUnless(predicate: (T) -> Boolean): T? {
    return if (!predicate(this)) this else null
}

takeUnless 和 takeIf 正好相反,若是 predicate 返回 false 就返回它自己,不然返回 null 。

總結

這麼多的語法糖,其實他們的原理都是相似的,共同點在於都是有返回值的,而區別就在於對原有的值進行了哪些操做,而後如何返回最終的值。

最後,光是瞭解他們的原理和調用狀況仍是不夠的,再不影響代碼閱讀的狀況下要把它們引入到咱們的代碼中去,靈活地使用它們。

歡迎掃描關注微信公衆號:【紙上淺談】,得到最新文章推送~~~

紙上淺談

相關文章
相關標籤/搜索