原文標題: Mastering Kotlin standard functions: run, with, let, also and apply

有一些Kotlin的標準函數(shù)的功能很相似,有時候我們不確定應該使用哪個。下面我將介紹一種簡單的方式來區(qū)分它們的不同之處,以及如何確定應該使用哪個。
范圍函數(shù)
我今天要講述的是關于 run \ with \ T.run \ T.let \ T.also \ T.apply. 我把它們叫做 范圍函數(shù), 因為我認為它們的主要功能在于為調(diào)用這些函數(shù)的對象提供了不同的作用域。
下面是一種最簡單的方式來描述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
}
<我注: 輸出結果>
I am happy
I am sad
在上面代碼的test函數(shù)中, 你可以使用run關鍵字定義一個單獨的代碼塊, 在這個代碼塊中在打印輸出之前將mood變量的值改為I am happy. 同時在run代碼塊中定義的mood的值只能作用于這個代碼塊. 因為你會發(fā)現(xiàn)在run代碼塊之外, 再去打印mood 輸出的是 I am sad.
限定變量作用域的這個功能本身并沒有太大用處. 但是除此之外他有另外一個有趣的功能點, 那就是他還可以有返回值: 返回在代碼塊范圍內(nèi)修改后的對象.
如此以來下面的代碼看起來就比較整潔:
run {
if (firstTimeView) introView else normalView
}.show()
這段代碼中, run代碼塊根據(jù)不同的條件返回了不同的對象, 然后調(diào)用不同對象的show()方法. 這樣我們就不必單獨維護兩個變量來分別調(diào)用他們的show方法.
范圍函數(shù)的3種特性
為了讓范圍函數(shù)更有意思, 我把他們的不同表現(xiàn)總結為3種特性. 我將使用這些特性來把他們區(qū)分開.
1. 普通函數(shù) vs. 擴展函數(shù) (normal vs. extension function)
如果我們觀察 with 和 T.run, 我們發(fā)現(xiàn)他們兩個實際作用很相似. 比如下面這段代碼:
with(webview.settings) {
javaScriptEnabled = true
databaseEnabled = true
}
// similarly
webview.settings.run {
javaScriptEnabled = true
databaseEnabled = true
}
上面代碼中用with 和 T.run 實現(xiàn)了同樣的功能. 但是他們的不同之處在于: with是一個普通函數(shù), 而T.run則是一個擴展函數(shù).
那么問題來了, 這兩種用法各自的優(yōu)點是什么?
我們假設 webview.settings 這個變量的值有可能為null的話, 他們的不同點就體現(xiàn)出來了:
// Yack! -- 代碼塊中在對webview.settings對象進行操作之前都需要進行判空操作
with(webview.settings) {
this?.javaScriptEnabled = true
this?.databaseEnabled = true
}
// Nice.
webview.settings?.run {
javaScriptEnabled = true
databaseEnabled = true
}
在這個例子中, 很明顯 T.run 這種擴展函數(shù)的方式更好, 因為我們可以在使用對象之前, 對他進行全局的判空操作. (<我注:>而with那種方式需要在代碼塊中逐句判空)
2. this 和 it 參數(shù)
如果我們觀察 T.run 和 T.let, 這兩個函數(shù)的作用非常相似除了一點: 他們訪問參數(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就是用擴展函數(shù)的方式調(diào)用了block: T.(). 所以在T.run函數(shù)的代碼塊中, 可以使用this關鍵字來得到對主變量T的引用. 在實際編程中, 通過this關鍵字的調(diào)用通??梢圆粚?code>this.. 所以在上面的示例代碼中, 我們直接使用了println($length) 而不是println(${this.length}). 我將這種方式稱為使用this作為參數(shù)的函數(shù)調(diào)用.
<我注: T.run的源碼>
/**
* Calls the specified function [block] with `this` value as its receiver and returns its result.
*/
@kotlin.internal.InlineOnly
public inline fun <T, R> T.run(block: T.() -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return block()
}
然而如果去看T.let函數(shù)的源碼, 你會發(fā)現(xiàn)T.let是把主變量自己作為參數(shù)調(diào)用代碼塊: block: (T). 看起來像是使用lambda參數(shù)進行函數(shù)調(diào)用. 這種方式在代碼塊中是使用 it 來引用主變量. 所以我將這種方式稱為: 使用it作為參數(shù)的函數(shù)調(diào)用.
<我注: T.let的源碼>
/**
* Calls the specified function [block] with `this` value as its argument and returns its result.
*/
@kotlin.internal.InlineOnly
public inline fun <T, R> T.let(block: (T) -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return block(this)
}
從上面的論述中, 看起來T.run用起來比T.let更方便些, 因為使用T.run我們可以直接隱式使用this訪問主變量, 而T.let需要主動指定使用it才能訪問主變量. 但是使用T.let函數(shù)還有一些細微的好處:
-
T.let提供了一種更清晰的方式來區(qū)分訪問的屬性或方法是來自于調(diào)用函數(shù)的主變量, 還是來自其他外部的變量. - 當
this需要顯示傳遞的時候: 比如在調(diào)用另外的方法需要把this作為參數(shù)傳遞過去, 這種情況下, 使用it(2個字母) 比使用this(4個字母) 更短, 也更清晰. -
T.let允許在作用域范圍內(nèi)對it重命名為更加有意義的變量名稱. 比如:
stringVariable?.let {
nonNullString ->
println("The non null string is $nonNullString")
}
3. 返回 this 或是 其他類型
現(xiàn)在我們來看T.let和T.also, 他們在內(nèi)部的函數(shù)作用域方面是相同的. 比如:
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(代碼塊的主變量).
T.let 和 T.also在鏈式調(diào)用方面都非常好用, T.let可以將操作之后的結果返回, T.also可以在主變量上進行操作然后再返回this主變量.
下面是對T.let 和 T.also的簡單示例代碼:
val original = "abc"
// 改變主變量的值并向后傳遞
original.let {
println("The original String is $it") // "abc"
it.reversed() // 將it的內(nèi)容反轉(zhuǎn)并傳遞到下一步
}.let {
println("The reverse String is $it") // "cba"
it.length // it的值類型從string轉(zhuǎn)變?yōu)閕nt
}.let {
println("The length of the String is $it") // 3
}
// 錯誤示例
// 整個鏈上都是同樣的值 (打印結果與期望不同)
original.also {
println("The original String is $it") // "abc"
it.reversed() // 盡管把it的值反轉(zhuǎn)了 但他并沒有把反轉(zhuǎn)后的結果傳遞到下一步
}.also {
println("The reverse String is ${it}") // "abc"
it.length // 盡管返回了it的長度但這個值并沒有傳遞到下一步
}.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 似乎看起來毫無意義, 因為我們可以直接將他們放到一個單獨的代碼塊中即可實現(xiàn)相同的功能. 其實仔細考慮一下, T.also還是有一些好處的:
- 他可以讓整個鏈上的操作過程顯得更加清晰: 將整個操作分開到不同的更小的代碼塊中來完成
- 在使用對象之前分步驟對self進行操作來使用鏈式構造, 此時
T.also會顯的更加方便易用.
當把這兩個函數(shù)聯(lián)合使用時(一個在this的基礎上改進并返回, 一個保持this的引用并返回), 范圍函數(shù)的功能會更加強大, 比如:
// 常規(guī)方式
fun makeDir(path: String): File {
val result = File(path)
result.mkdirs()
return result
}
// 使用 `let` / `also`的改進方式
fun makeDir(path: String) = path.let{ File(it) }.also{ it.mkdirs() }
其他特性
通過上述對3種特性的描述, 我們了解了各自函數(shù)的特性. 現(xiàn)在來說說尚未提及的 T.apply 函數(shù), 這個函數(shù)相應的特性如下:
- 他是一個擴展函數(shù)
- 跟
T.run類似,T.apply也是傳遞this到代碼塊 - 跟
T.also類似,T.apply也是返回this的引用
因此一個可以想象到的應用方式如下:
// 普通方式
fun createInstance(args: Bundle) : MyFragment {
val fragment = MyFragment()
fragment.arguments = args
return fragment
}
// 使用 `apply` 改進后的方式
fun createInstance(args: Bundle)
= MyFragment().apply { arguments = args }
或者我們可以把不可鏈式調(diào)用的代碼變成鏈式調(diào)用的代碼風格:
// 普通方式 非鏈式調(diào)用
fun createIntent(intentData: String, intentAction: String): Intent {
val intent = Intent()
intent.action = intentAction
intent.data=Uri.parse(intentData)
return intent
}
// 改進后的鏈式調(diào)用風格
fun createIntent(intentData: String, intentAction: String) =
Intent().apply { action = intentAction }
.apply { data = Uri.parse(intentData) }
如何選擇使用哪個函數(shù)
通過上述對各個函數(shù)的3種特性的描述, 我們可以對他們進行歸類. 基于上述特性, 我們可以總結出下面的決策樹來幫助我們根據(jù)具體需求來決定應該使用哪個函數(shù).

希望上面的決策樹插圖能將這些標準函數(shù)的特性描述的更清晰一些, 希望能使你更方便的決定應該使用哪個函數(shù)來操作, 同時更好的掌握對這些函數(shù)的恰當使用.
很愿意聽到大家提供對這些標準函數(shù)的真事使用場景, 大家一起討論一起進步.
希望你能通過這篇文章理解到這些標準函數(shù)的使用方式, 如果你感覺有幫助可以分享給你的程友.