學會使用Kotlin標準函數(shù):run / with / let 和 apply

原文鏈接: https://medium.com/@elye.project/mastering-kotlin-standard-functions-run-with-let-also-and-apply-9cd334b0ef84

原文標題: Mastering Kotlin standard functions: run, with, let, also and apply

1*9nUzj5iRxj_Hddni6ob28w.png

有一些Kotlin的標準函數(shù)的功能很相似,有時候我們不確定應該使用哪個。下面我將介紹一種簡單的方式來區(qū)分它們的不同之處,以及如何確定應該使用哪個。

范圍函數(shù)

我今天要講述的是關于 run \ with \ T.run \ T.let \ T.also \ T.apply. 我把它們叫做 范圍函數(shù), 因為我認為它們的主要功能在于為調(diào)用這些函數(shù)的對象提供了不同的作用域。

下面是一種最簡單的方式來描述run函數(shù)的作用域:

fun test() {
    var mood = "I am sad"

    run {
        val mood = "I am happy"
        println(mood) // I am happy
    }
    println(mood)  // I am sad
}

<我注: 輸出結果>

I am happy
I am sad

在上面代碼的test函數(shù)中, 你可以使用run關鍵字定義一個單獨的代碼塊, 在這個代碼塊中在打印輸出之前將mood變量的值改為I am happy. 同時在run代碼塊中定義的mood的值只能作用于這個代碼塊. 因為你會發(fā)現(xiàn)在run代碼塊之外, 再去打印mood 輸出的是 I am sad.

限定變量作用域的這個功能本身并沒有太大用處. 但是除此之外他有另外一個有趣的功能點, 那就是他還可以有返回值: 返回在代碼塊范圍內(nèi)修改后的對象.

如此以來下面的代碼看起來就比較整潔:

run {
    if (firstTimeView) introView else normalView
}.show()

這段代碼中, run代碼塊根據(jù)不同的條件返回了不同的對象, 然后調(diào)用不同對象的show()方法. 這樣我們就不必單獨維護兩個變量來分別調(diào)用他們的show方法.

范圍函數(shù)的3種特性

為了讓范圍函數(shù)更有意思, 我把他們的不同表現(xiàn)總結為3種特性. 我將使用這些特性來把他們區(qū)分開.

1. 普通函數(shù) vs. 擴展函數(shù) (normal vs. extension function)

如果我們觀察 withT.run, 我們發(fā)現(xiàn)他們兩個實際作用很相似. 比如下面這段代碼:

with(webview.settings) {
    javaScriptEnabled = true
    databaseEnabled = true
}
// similarly
webview.settings.run {
    javaScriptEnabled = true
    databaseEnabled = true
}

上面代碼中用withT.run 實現(xiàn)了同樣的功能. 但是他們的不同之處在于: with是一個普通函數(shù), 而T.run則是一個擴展函數(shù).

那么問題來了, 這兩種用法各自的優(yōu)點是什么?

我們假設 webview.settings 這個變量的值有可能為null的話, 他們的不同點就體現(xiàn)出來了:

// Yack! -- 代碼塊中在對webview.settings對象進行操作之前都需要進行判空操作
with(webview.settings) {
      this?.javaScriptEnabled = true
      this?.databaseEnabled = true
}
// Nice.
webview.settings?.run {
    javaScriptEnabled = true
    databaseEnabled = true
}

在這個例子中, 很明顯 T.run 這種擴展函數(shù)的方式更好, 因為我們可以在使用對象之前, 對他進行全局的判空操作. (<我注:>而with那種方式需要在代碼塊中逐句判空)

2. thisit 參數(shù)

如果我們觀察 T.runT.let, 這兩個函數(shù)的作用非常相似除了一點: 他們訪問參數(shù)的方式不同. 下面這段代碼是使用不同的方式訪問各自代碼塊的主變量:

stringVariable?.run {
      println("The length of this String is $length")
}
// Similarly.
stringVariable?.let {
      println("The length of this String is ${it.length}")
}

如果你去檢查T.run函數(shù)的源碼, 你會發(fā)現(xiàn)T.run就是用擴展函數(shù)的方式調(diào)用了block: T.(). 所以在T.run函數(shù)的代碼塊中, 可以使用this關鍵字來得到對主變量T的引用. 在實際編程中, 通過this關鍵字的調(diào)用通??梢圆粚?code>this.. 所以在上面的示例代碼中, 我們直接使用了println($length) 而不是println(${this.length}). 我將這種方式稱為使用this作為參數(shù)的函數(shù)調(diào)用.

<我注: T.run的源碼>

/**
 * Calls the specified function [block] with `this` value as its receiver and returns its result.
 */
@kotlin.internal.InlineOnly
public inline fun <T, R> T.run(block: T.() -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block()
}

然而如果去看T.let函數(shù)的源碼, 你會發(fā)現(xiàn)T.let是把主變量自己作為參數(shù)調(diào)用代碼塊: block: (T). 看起來像是使用lambda參數(shù)進行函數(shù)調(diào)用. 這種方式在代碼塊中是使用 it 來引用主變量. 所以我將這種方式稱為: 使用it作為參數(shù)的函數(shù)調(diào)用.

<我注: T.let的源碼>

/**
 * Calls the specified function [block] with `this` value as its argument and returns its result.
 */
@kotlin.internal.InlineOnly
public inline fun <T, R> T.let(block: (T) -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block(this)
}

從上面的論述中, 看起來T.run用起來比T.let更方便些, 因為使用T.run我們可以直接隱式使用this訪問主變量, 而T.let需要主動指定使用it才能訪問主變量. 但是使用T.let函數(shù)還有一些細微的好處:

  • T.let提供了一種更清晰的方式來區(qū)分訪問的屬性或方法是來自于調(diào)用函數(shù)的主變量, 還是來自其他外部的變量.
  • this需要顯示傳遞的時候: 比如在調(diào)用另外的方法需要把this作為參數(shù)傳遞過去, 這種情況下, 使用it(2個字母) 比使用this(4個字母) 更短, 也更清晰.
  • T.let 允許在作用域范圍內(nèi)對it重命名為更加有意義的變量名稱. 比如:
stringVariable?.let {
      nonNullString ->
      println("The non null string is $nonNullString")
}

3. 返回 this 或是 其他類型

現(xiàn)在我們來看T.letT.also, 他們在內(nèi)部的函數(shù)作用域方面是相同的. 比如:

stringVariable?.let {
      println("The length of this String is ${it.length}")
}
// Exactly the same as below
stringVariable?.also {
      println("The length of this String is ${it.length}")
}

然而他們細微的差別在于各自的返回值. T.let 可以返回一個不同的對象, 而T.also返回了T 也就是this(代碼塊的主變量).

T.letT.also在鏈式調(diào)用方面都非常好用, T.let可以將操作之后的結果返回, T.also可以在主變量上進行操作然后再返回this主變量.

下面是對T.letT.also的簡單示例代碼:

val original = "abc"
// 改變主變量的值并向后傳遞
original.let {
    println("The original String is $it") // "abc"
    it.reversed() // 將it的內(nèi)容反轉(zhuǎn)并傳遞到下一步
}.let {
    println("The reverse String is $it") // "cba"
    it.length  // it的值類型從string轉(zhuǎn)變?yōu)閕nt
}.let {
    println("The length of the String is $it") // 3
}
// 錯誤示例
// 整個鏈上都是同樣的值 (打印結果與期望不同)
original.also {
    println("The original String is $it") // "abc"
    it.reversed() // 盡管把it的值反轉(zhuǎn)了 但他并沒有把反轉(zhuǎn)后的結果傳遞到下一步
}.also {
    println("The reverse String is ${it}") // "abc"
    it.length  // 盡管返回了it的長度但這個值并沒有傳遞到下一步
}.also {
    println("The length of the String is ${it}") // "abc"
}
// 使用 `also` 來得到相同的結果 (也就是在原來字符串的基礎上進行操作
// 整個鏈上傳遞的值都是一樣的
original.also {
    println("The original String is $it") // "abc"
}.also {
    println("The reverse String is ${it.reversed()}") // "cba"
}.also {
    println("The length of the String is ${it.length}") // 3
}

上面的 T.also 似乎看起來毫無意義, 因為我們可以直接將他們放到一個單獨的代碼塊中即可實現(xiàn)相同的功能. 其實仔細考慮一下, T.also還是有一些好處的:

  1. 他可以讓整個鏈上的操作過程顯得更加清晰: 將整個操作分開到不同的更小的代碼塊中來完成
  2. 在使用對象之前分步驟對self進行操作來使用鏈式構造, 此時T.also會顯的更加方便易用.

當把這兩個函數(shù)聯(lián)合使用時(一個在this的基礎上改進并返回, 一個保持this的引用并返回), 范圍函數(shù)的功能會更加強大, 比如:

// 常規(guī)方式
fun makeDir(path: String): File  {
    val result = File(path)
    result.mkdirs()
    return result
}
// 使用 `let` / `also`的改進方式
fun makeDir(path: String) = path.let{ File(it) }.also{ it.mkdirs() }

其他特性

通過上述對3種特性的描述, 我們了解了各自函數(shù)的特性. 現(xiàn)在來說說尚未提及的 T.apply 函數(shù), 這個函數(shù)相應的特性如下:

  1. 他是一個擴展函數(shù)
  2. T.run類似, T.apply也是傳遞this到代碼塊
  3. T.also類似, T.apply也是返回this的引用

因此一個可以想象到的應用方式如下:

// 普通方式
fun createInstance(args: Bundle) : MyFragment {
    val fragment = MyFragment()
    fragment.arguments = args
    return fragment
}
// 使用 `apply` 改進后的方式
fun createInstance(args: Bundle) 
              = MyFragment().apply { arguments = args }

或者我們可以把不可鏈式調(diào)用的代碼變成鏈式調(diào)用的代碼風格:

// 普通方式 非鏈式調(diào)用
fun createIntent(intentData: String, intentAction: String): Intent {
    val intent = Intent()
    intent.action = intentAction
    intent.data=Uri.parse(intentData)
    return intent
}
// 改進后的鏈式調(diào)用風格
fun createIntent(intentData: String, intentAction: String) =
        Intent().apply { action = intentAction }
                .apply { data = Uri.parse(intentData) }

如何選擇使用哪個函數(shù)

通過上述對各個函數(shù)的3種特性的描述, 我們可以對他們進行歸類. 基于上述特性, 我們可以總結出下面的決策樹來幫助我們根據(jù)具體需求來決定應該使用哪個函數(shù).

1*pLNnrvgvmG6Mdi0Yw3mdPQ.png

希望上面的決策樹插圖能將這些標準函數(shù)的特性描述的更清晰一些, 希望能使你更方便的決定應該使用哪個函數(shù)來操作, 同時更好的掌握對這些函數(shù)的恰當使用.

很愿意聽到大家提供對這些標準函數(shù)的真事使用場景, 大家一起討論一起進步.

希望你能通過這篇文章理解到這些標準函數(shù)的使用方式, 如果你感覺有幫助可以分享給你的程友.


再次注明:
原文鏈接: https://medium.com/@elye.project/mastering-kotlin-standard-functions-run-with-let-also-and-apply-9cd334b0ef84

?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容