[譯]精通Kotlin標準函式:run、with、let、also和apply

liangfei發表於2019-03-04

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

一些 Kotlin 的標準函式非常相似,以至於我們都無法確定要使用哪一個。這裡我會介紹一種簡單的方式來區分他們的不同點以及如何選擇使用。

作用域函式

接下來聚焦的函式有:runwithT.runT.letT.also 以及 T.apply。我稱他們為作用域函式(scoping functions),因為它們為呼叫方函式提供了一個內部作用域。

最能夠體現作用域的是 run 函式:

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

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

    println(mood) // I am sad
}
複製程式碼

基於此,在 test 函式內部,你可以擁有一個單獨的區域,在這個作用域內,mood 在列印之前被重新定義成了 I am happy,並且它完全被包裹(enclosed)在 run 的區域內。

這個作用域函式本身看起來並不會非常有用。但是除了擁有單獨的區域之外,它還有另一個優勢:它有返回值,即區域內的最後一個物件。

因此,下面的程式碼會變得整潔,我們把 show() 函式應用到兩個 view 之上,但是並不需要呼叫兩次。

run {
    if (firstTimeView) introView else normalView
}.show()
複製程式碼

這裡演示所用,其實還可以簡化為 (if (firstTimeView) introView else normalView).show()

作用域函式三大特性

為了讓作用域函式更有意思,可將其行為分類為三大特性。我會使用這些特性來區分彼此。

一、正常 vs. 擴充套件函式

如果我們看一下 withT.run,會發現它們的確非常相似。下面的程式碼做了同樣的事情。

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

// similarly

webview.settings.run {
    javaScriptEnabled = true
    databaseEnabled = true
}
複製程式碼

但是,它們的不同點在於,一個是正常函式(即 with),另一個是擴充套件函式(即 T.run)。

假設 webview.settings 可能為空,那麼程式碼就會變成下面的樣子:

// Yack!
with(webview.settings) {
    this?.javaScriptEnabled = true
    this?.databaseEnabled = true
}

// Nice
webview.settings?.run {
    javaScriptEnabled = true
    databaseEnabled = true
}
複製程式碼

在這個案例中,T.run 的擴充套件函式明顯要好一些,因為我們可以在使用前就做好了空檢查。

二、this vs. it 引數

如果我們看一下 T.runT.let,會發現兩個函式是相似的,只有一點不同:它們接收引數的方式。下面程式碼展示了用兩個函式實現同樣的邏輯:

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

// Similarly

stringVariable?.let {
    println("The length of this String is ${it.length}")
}
複製程式碼

如果檢查一下 T.run 的函式簽名就會發現 T.run 只是一個呼叫 block: T.() 的擴充套件函式。因此在它的作用域內,T 可以被引用為 this。實際程式設計中,this 大部分情況下都可以被省略。因此,在上面的例子中,我們可以在 println 的宣告語句中使用 $length 而不是 ${this.length}。我把它稱之為:this 作為引數進行傳遞。

但是,對於 T.let 函式,你會發現 T.let 把它自己傳入了函式 block: (T)。因此它被當做一個 lambda 引數來傳遞。在作用域函式內它可以被引用為 it。所以我稱之為:it 作為引數進行傳遞。

從上面可以看出,T.run 好像比 T.let 高階,因為它更隱式一些,但是 T.let 函式會有些一些微妙的優勢:

  • T.let 可以更清楚地區分所得變數和外部類的函式/成員。

  • this 不能被省略的情況下,例如用作一個函式引數,itthis 更短更清晰。

  • T.let 允許用更好的命名來表示轉換過的所用變數(the converted used variable),也就是說,你可以把 it 轉換為其他名字:

    stringVariable?.let {
        nonNullString ->
        println("The non null string is $nonNullString")
    }
    複製程式碼

三、返回 this vs. 其他型別

現在,我們看一下 T.letT.also,如果我們看一下函式作用域內部的話,會發現兩者是一樣的:

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

簡單的示例如下:

val original = "abc"

// Evolve the value and send to the next chain
original.let {
    println("The original String is $it") // "abc"
    it.reversed() // evolve it as parameter to send to next let
}.let {
    println("The reverse String is $it") // "cba"
    it.length // can be evolve to other type
}.let {
    println("The length of the String is $it") // 3
}

// Wrong
// Same value is sent in the chain (printed answer is wrong)
original.also {
    println("The original String is $it") // "abc"
    it.reversed() // even if we evolve it, it is useless
}.also {
    println("The reverse String is ${it}") // "abc"
    it.length // even if we evolve it, it is useless
}.also {
    println("The length of the String is ${it}") // "abc"
}

// Corrected for also (i.e. manipulate as original string
// Same value is sent in the chain
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 貌似沒什麼意義,因為我們可以輕鬆把它們組合進一個單一的函式塊內。仔細想一下,它們會有如下優勢:

  • 它可以為相同的物件提供清晰的處理流程,可以使用粒度更小的函式式部分。
  • 它可以在被使用之前做靈活的自處理(self manipulation),可以建立一個鏈式構造器操作。

如果兩者結合鏈式來使用,一個進化自己,一個持有自己,就會變得非常強大,例如:

// Normal approach
fun makeDir(path: String): File {
    val result = File(path)
    result.mkdirs()
    return result
}

// Improved approach
fun makeDir(path: String) = path.let{ File(it) }.also{ it.mkdirs() }
複製程式碼

回顧一下所有的特性

通過這三個特性,我們可以清楚地知道每個函式的行為。讓我們舉例說明一下上面沒有提到的 T.apply 函式,它的 3 個特性如下所述:

  • 它是一個擴充套件函式
  • 它把 this 作為引數
  • 它返回了 this(它自己)
// Normal approach
fun createIntent(intentData: String, intentAction: String): Intent {
    val intent = Intent()
    intent.action = intentAction
    intent.data = Uri.parse(intentData)
    return intent
}

// Improved approach, chaining
fun createIntent(intentData: String, intentAction: String) = 
    Intent().apply { action = intentAction }
            .apply { data = Uri.parse(intentData) }
複製程式碼

或者我們也可以把一個非鏈式的物件建立過程變得可鏈式(chain-able):

// Normal approach
fun createIntent(intentData: String, intentAction: String): Intent {
    val intent = Intent()
    intent.action = intentAction
    intent.data = Uri.parse(intentData)
    return intent
}

// Improved approach, chaining
fun createIntent(intentData: String, intentAction: String) = 
    Intent().apply { action = intentAction }
            .apply { data = Uri.parse(intentData) }
複製程式碼

函式選擇

現在思路變清晰了,根據這三大特性,我們可以對函式進行分類。基於此可以構建一個決策樹來幫助我們根據需要來選擇使用哪一個函式。

[譯]精通Kotlin標準函式:run、with、let、also和apply

希望上面的決策樹能夠更清晰地闡述這些函式,同時也能簡化你的決策,使你能夠得當地使用這些函式。

相關文章