掌握Kotlin標準函式:run, with, let, also and apply

adison發表於2019-03-04

原文連結

Kotlin的一些標準函式非常相似,我們不確定使用哪個函式。在這裡我將介紹一個簡單的方法來清楚地區分他們的差異和如何選擇使用。

範圍函式

我重點關注run, with, T.run, T.let, T.also and T.apply函式。我稱他們為範圍函式,因為我認為他們的主要功能是為呼叫函式提供一個內部範圍。

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並列印之前,它被完全封閉在run範圍內。

這個範圍函式本身似乎不是很有用。但是相比範圍,還有一點不錯的是,它返回範圍內最後一個物件。

因此,下面程式碼將是很純潔的,我們可以像下面一樣,將show()方法應用到兩個 view,而不是 呼叫兩次。

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

範圍函式的3個屬性

為了使範圍函式更有趣,讓我用3個屬性將他們的行為分類,並且使用這些屬性來區分它們。

1.正常vs.擴充套件函式

如果我們看看定義,with並且T.run這兩個函式實際上非常相似。下面示例實現功能是一樣的。

with(webview.settings) {
    javaScriptEnabled = true
    databaseEnabled = true
}
// 相似
webview.settings.run {
    javaScriptEnabled = true
    databaseEnabled = true
}
複製程式碼

然而,它們的不同之處在於with是正常函式,而T.run是擴充套件函式。

那麼問題是,每個的優點是什麼?

想象一下,如果webview.settings可能是空的,那麼看起來就像下面一樣了。

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

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

在這種情況下,顯然T.run擴充套件功能比較好,因為在使用之前我們可以判空。

2.This vs. it引數

如果我們看看定義,,T.run並且T.let這兩個函式除了接受引數的方式不一樣外幾乎是一樣的。以下兩個函式的邏輯是相同的。

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

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允許使用更好的變數命名,你可以轉換it為其他名稱。
stringVariable?.let {
      nonNullString ->
      println("The non null string is $nonNullString")
}
複製程式碼

3.返回當前型別 vs.其他型別

現在,我們來看看T.letT.also,如果我們看它們的內部函式範圍,使用起來是一樣的

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

然而,他們微妙的不同是他們的返回值。T.let返回不同型別的值,而T.also返回T本身即this

兩者對於連結函式都是有用的,通過T.let你可以演變操作,通過T.also你在同一個變數this上執行操作。

簡單的例子如下

val original = "abc"
// 改變值並且傳遞到下一鏈條
original.let {
    println("The original String is $it") // "abc"
    it.reversed() // 改變引數並且傳遞到下一鏈條
}.let {
    println("The reverse String is $it") // "cba"
    it.length   // 改變型別
}.let {
    println("The length of the String is $it") // 3
}
// 錯誤
// 在鏈中傳送相同的值(列印的答案是錯誤的)
original.also {
    println("The original String is $it") // "abc"
    it.reversed() // 即使我們改變它,也是沒用的
}.also {
    println("The reverse String is ${it}") // "abc"
    it.length  // 即使我們改變它,也是沒用的
}.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好像毫無意義,因為我們可以很容易地將它們組合成一個功能塊。但仔細想想,它也有一些優點:

  1. 它可以在相同的物件上提供一個非常清晰的分離過程,即製作更小的功能部分。
  2. 在使用之前,它可以實現非常強大的自我操縱,實現鏈條建設者操作(builder 模式)。

當兩者結合在一起時,即一個自我演變,一個自我保留,可以變得非常強大,例如下面

// 正常方法
fun makeDir(path: String): File  {
    val result = File(path)
    result.mkdirs()
    return result
}
// 改進方法
fun makeDir(path: String) = path.let{ File(it) }.also{ it.mkdirs() }
複製程式碼

所有的屬性

通過說明這3個屬性,我們應該可以瞭解這些函式的行為了。讓我們再來看看T.apply函式,因為上面沒有提到。這3個屬性在T.apply定義如下…

  1. 這是一個擴充套件函式
  2. this作為引數傳遞。
  3. 它返回this(即它本身)

因此,可以想象,它可以像下面一樣被使用

// 正常方法
fun createInstance(args: Bundle) : MyFragment {
    val fragment = MyFragment()
    fragment.arguments = args
    return fragment
}
// 改進方法
fun createInstance(args: Bundle) 
              = MyFragment().apply { arguments = args }
複製程式碼

或者我們也可以建立鏈式呼叫。

// 正常方法
fun createIntent(intentData: String, intentAction: String): Intent {
    val intent = Intent()
    intent.action = intentAction
    intent.data=Uri.parse(intentData)
    return intent
}
//  改進實現
fun createIntent(intentData: String, intentAction: String) =
        Intent().apply { action = intentAction }
                .apply { data = Uri.parse(intentData) }
複製程式碼

函式選擇

因此,顯然,有了這三個屬性,我們現在可以對上述函式進行相應的分類。在此基礎上,我們可以在下面形成一個決策樹,可以幫助我們決定使用哪個函式。

img

希望上面的決策樹可以清晰說明函式區別,也簡化您的決策,使您能夠恰當掌握這些函式的使用。

相關文章