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

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

一些 Kotlin 的標準函數很是類似,以致於咱們都沒法肯定要使用哪個。這裏我會介紹一種簡單的方式來區分他們的不一樣點以及如何選擇使用。git

做用域函數

接下來聚焦的函數有:runwithT.runT.letT.also 以及 T.apply。我稱他們爲做用域函數(scoping functions),由於它們爲調用方函數提供了一個內部做用域。github

最可以體現做用域的是 run 函數:web

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 的區域內。編程

這個做用域函數自己看起來並不會很是有用。可是除了擁有單獨的區域以外,它還有另外一個優點:它有返回值,即區域內的最後一個對象。app

所以,下面的代碼會變得整潔,咱們把 show() 函數應用到兩個 view 之上,可是並不須要調用兩次。less

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

這裏演示所用,其實還能夠簡化爲 (if (firstTimeView) introView else normalView).show()函數

做用域函數三大特性

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

1、正常 vs. 擴展函數

若是咱們看一下 withT.run,會發現它們的確很是類似。下面的代碼作了一樣的事情。spa

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 的擴展函數明顯要好一些,由於咱們能夠在使用前就作好了空檢查。

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

3、返回 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) }
複製代碼

函數選擇

如今思路變清晰了,根據這三大特性,咱們能夠對函數進行分類。基於此能夠構建一個決策樹來幫助咱們根據須要來選擇使用哪個函數。

但願上面的決策樹可以更清晰地闡述這些函數,同時也能簡化你的決策,使你可以得當地使用這些函數。

相關文章
相關標籤/搜索