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ù)
我們看一下with和T.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.run 和 T.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.let和T.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ù):
