Kotlin中 run, with, let, also and apply 函式的分類與對比
Kotlin提供了run, with, let, also and apply等功能函式,使用這些功能函式可以提高程式碼的可讀性和簡潔性。比如如下的程式碼利用java實現是這樣的 :
public static int getPeopleTotalAge(People people) { if (people == null) { return 0; } int age = people.age; if (people.child1 != null) { age += people.child1.age; } if (people.child2 != null) { age += people.child2.age; } return age; }
如果用Kotlin來實現,就能變得很簡潔:
fun getPeopleTotalAge(people: JavaTest.People?) = people?.run { (child1?.age ?: 0) + (child2?.age ?: 0) + age } ?: 0
Kotlin中 run, with, let, also and apply在使用上有很多區別,我把這幾個函式又能分成如下三類。
三種類型
1.普通函式與擴充套件函式
with 和 T.run 很類似,比如下面的程式碼中,實現的功能一樣 :
with(people.settings) { javaScriptEnabled = true databaseEnabled = true } people.settings.run { javaScriptEnabled = true databaseEnabled = true }
但是區別在於 with 是一個普通函式,但是T.run 是一個擴充套件函式,那麼用哪個比較好呢? 假設 people.settings 可能為null,那上面的程式碼應該修改為如下 :
// 比較繁雜 with(people.settings) { this?.javaScriptEnabled = true this?.databaseEnabled = true } // 比較簡潔 people.settings?.run { javaScriptEnabled = true databaseEnabled = true }
所以根據具體使用場景選擇with 和 T.run,可以使程式碼更簡潔。另外要注意一下,run 函式也有普通函式的版本,與T.run 使用上沒有什麼區別,比如下面的程式碼 :
fun testRun(i: Int) = run { if (i < 0) { return 0 } i + 1 }
2.this引數與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.let需要通過it來呼叫屬性,如果我們檢視這兩個函式的原始碼 :
public inline fun <T, R> T.let(block: (T) -> R): R { contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } return block(this) } public inline fun <T, R> T.run(block: T.() -> R): R { contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } return block() }
可以發現,T.run 是通過擴充套件的方式來傳遞引數block: T.() -> R
;但是T.let 是傳遞一個函式block: (T) -> R
。所以T.run可以直接呼叫,但是T.let需要it呼叫。雖然T.run 看起來比T.let 更好用,但是在一些情況下更適合用T.let :
- T.let 明確了引數的關係,在呼叫引數時不會和其它全域性變數搞混;
-
T.let 中的it 也可以換名字,提高程式碼的可讀性;
stringVariable?.let { nonNullString -> println("The non null string is $nonNullString") }
另外,T.apply 與 T.also 也一樣,前者可以直接呼叫引數,後者需要通過it來呼叫引數。比如下面的程式碼 :
stringVariable?.apply { println("The length of this String is $length") } stringVariable?.also { println("The length of this String is ${it.length}") }
3.返回this或返回最後一行
T.let 和 T.also 使用上是差不多的,通過前面的分析我們知道都是通過it來呼叫引數。但是在下面的例子中,它們還是有些許不同:
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 } //錯誤,一直返回"abc" 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" } //正確,結果與呼叫T.let一致 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 返回的是this,即”abc”,所以像T.let鏈式呼叫的時候,一直返回”abc”,導致程式碼邏輯錯誤。T.also比較適合運用於鏈式呼叫,比如AlertDialog.Builder建立對話方塊時的呼叫方式。混合運用T.let和T.also能使程式碼更加簡潔,比如下面的程式碼 :
// 一般實現方式 fun makeDir(path: String): File{ val result = File(path) result.mkdirs() return result } // 改進後的方式 fun makeDir(path: String) = path.let{ File(it) }.also{ it.mkdirs() }
同理,T.apply 和 T.run 也是這種型別的組合,T.apply 返回this,而T.run 返回的是最後一行。
總結
通過前面的分類對比,我們知道這些函式可以分成三類:
- 普通函式與擴充套件函式 (with 與 T.run)
- this引數與it引數(T.run 與 T.let、T.apply 與 T.also)
- 返回this或返回最後一行(T.let 和 T.also、T.apply 與 T.run)
我們可以整理出一個表格,明確了各個函式的使用方式:
\ | 是否擴充套件函式 | 呼叫引數方式 | 返回值 |
---|---|---|---|
with | 是 | this | 最後一行 |
T.run | 否 | this | 最後一行 |
T.let | 否 | it | 最後一行 |
T.apply | 否 | this | this |
T.also | 否 | it | this |