Kotlin標準庫函數(shù): run,let,also,apply,with

Kotlin標準庫函數(shù): run,let,also,apply,with

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

作用域函數(shù)

run, with(T), T.run, T.let, T.also, T.apply都支持閉包作為參數(shù); 但是他們?yōu)檎{(diào)用者,在閉包內(nèi)部提供了一個內(nèi)部作用域, 我稱他們?yōu)?strong>作用域函數(shù)(scoping functions);

最明顯的是 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
    }

run函數(shù)的區(qū)域內(nèi), mood變量被重新定義, 并不和外部的定義沖突, 并且定義之后的run函數(shù)的范圍內(nèi), mood變量會覆蓋外部定義;

這么看上去,除了方法內(nèi)擁有單獨的作用域外, 并非特別有用; 但是這些方法還有一個特點, 它們擁有返回值;
eg. 'run'的返回值是區(qū)域內(nèi)的最后一個對象;

使用這個特性,可以使我們的代碼更整潔:
eg.我們想選擇對某個view調(diào)用show()方法, 并不需要對每個view都進行調(diào)用,

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

作用域函數(shù)的特點

1. 正常 vs. 擴展函數(shù)

我們看一下withT.run函數(shù), 會發(fā)現(xiàn)它們非常相似; 下面的代碼,做了相同的事:

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

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

然而,with是一個正常的方法, 而T.run是一個擴展函數(shù);

如果 webview.settings 可能為null, 那代碼就會變成下面這樣:

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

    // Nice.
    webview.settings?.run {
        javaScriptEnabled = true
        databaseEnabled = true
    }

上面這個例子中, 明顯T.run的擴展函數(shù)要好一些, 在使用變量之前,就做了非空檢查; 而with內(nèi)部需要每個都做檢查;

2. this vs. it變量

上面說過, 這些函數(shù)內(nèi)部, 新定義的變量都有單獨的作用域, 不和外部沖突; 而這個作用域中, 最特殊的就是this 和 it;

如果我們看T.runT.let, 會發(fā)現(xiàn)代碼非常相似, 但是有一點不同, 內(nèi)部使用的參數(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是調(diào)用函數(shù) block: T.()的擴展函數(shù), 相當于給對象 T 添加了一個方法; 因此, 在它的作用域中, T 可以被引用為this; 而在實際變成中, this大部分情況下可以省略; 在上面代碼中, println中的$length, 實際上就是${this.length}. 這種我稱之為this參數(shù)傳遞;

T.let的函數(shù)定義中, 你會發(fā)現(xiàn)T.let把它自己作為一個參數(shù). 傳遞給了函數(shù) block: (T); 而在lambda表達式中, 一個參數(shù)可以省略, 使用it代替; 因此,在作用域中, T被引用為了it; 這種我稱之為**it參數(shù)傳遞;

從上面看, T.run好像比T.let更高級一點, 在T.run中可以隱式的使用this代替自身; 但是在部分情況下, T.let更合適一點;

  • T.let更容易區(qū)分當前作用域的函數(shù)/變量和外部類的函數(shù)/變量

  • this不能被省略的地方, it相比this更加清晰簡短

  • T.let中可以使用更好的變量命名 (it是lambda省略參數(shù)的指代, 因此可以把it轉換為其他名字)

      stringVariable?.let {
          nonNullString ->
          println("The non null string is $nonNullString")
      }
    

2. 返回 this vs. 其他類型(block()函數(shù)的返回值)

看一下T.letT.also, 如果只看函數(shù)作用域的代碼, 會發(fā)現(xiàn)他們是一模一樣的:

    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的返回值,是對應調(diào)用的lambda表達式的返回值; 但是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似乎沒什么作用, 我們可以把幾個also的代碼合并到一個函數(shù)塊中, 但是細想一下, 會有下面幾個優(yōu)勢:

  • 它可以為同一個對象, 提供更加清晰的處理流程, 提供更細力度的函數(shù)控制
  • 它可以構建鏈式調(diào)用

如果兩者結合使用, 使用T.let升級自身, T.also持有自身進行鏈式調(diào)用, 將會變得非常強大:

    // 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() }

總結

通過上面3個特性, 我們可以更好的理解這幾個函數(shù)的行為;
比如上面沒提到的T.apply函數(shù), 它的性質如下:

  • 它是一個擴展函數(shù)
  • 它把this作為參數(shù)傳遞, 在函數(shù)體內(nèi), this指代調(diào)用者
  • 它返回this, 即返回調(diào)用者自身

使用如下:

    // Normal approach
    fun createInstance(args: Bundle) : MyFragment {
        val fragment = MyFragment()
        fragment.arguments = args
        return fragment
    }

    // Improved approach
    fun createInstance(args: Bundle) = MyFragment().apply { arguments = args }

我們也可以用它把一個非鏈式調(diào)用的過程,變?yōu)殒準秸{(diào)用:

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

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

根據(jù)函數(shù)的特點, 我們可以對函數(shù)進行分類, 構建一個決策樹幫助我們選擇使用哪個函數(shù):

kotlin_standard_function_selections.png

參考:

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

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

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