原文地址:https://medium.com/@elye.project/mastering-kotlin-standard-functions-run-with-let-also-and-apply-9cd334b0ef84java
一些 Kotlin 的標準函數很是類似,以致於咱們都沒法肯定要使用哪個。這裏我會介紹一種簡單的方式來區分他們的不一樣點以及如何選擇使用。git
接下來聚焦的函數有:run
、with
、T.run
、T.let
、T.also
以及 T.apply
。我稱他們爲做用域函數(scoping functions),由於它們爲調用方函數提供了一個內部做用域。github
最可以體現做用域的是 run
函數:web
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
,而且它徹底被包裹(enclosed)在 run
的區域內。編程
這個做用域函數自己看起來並不會很是有用。可是除了擁有單獨的區域以外,它還有另外一個優點:它有返回值,即區域內的最後一個對象。app
所以,下面的代碼會變得整潔,咱們把 show()
函數應用到兩個 view 之上,可是並不須要調用兩次。less
run {
if (firstTimeView) introView else normalView
}.show()
複製代碼
這裏演示所用,其實還能夠簡化爲
(if (firstTimeView) introView else normalView).show()
。函數
爲了讓做用域函數更有意思,可將其行爲分類爲三大特性。我會使用這些特性來區分彼此。this
若是咱們看一下 with
和 T.run
,會發現它們的確很是類似。下面的代碼作了一樣的事情。spa
with(webview.settings) {
javaScriptEnabled = true
databaseEnabled = true
}
// similarly
webview.settings.run {
javaScriptEnabled = true
databaseEnabled = true
}
複製代碼
可是,它們的不一樣點在於,一個是正常函數(即 with
),另外一個是擴展函數(即 T.run
)。
假設 webview.settings
可能爲空,那麼代碼就會變成下面的樣子:
// Yack!
with(webview.settings) {
this?.javaScriptEnabled = true
this?.databaseEnabled = true
}
// Nice
webview.settings?.run {
javaScriptEnabled = true
databaseEnabled = true
}
複製代碼
在這個案例中,T.run
的擴展函數明顯要好一些,由於咱們能夠在使用前就作好了空檢查。
若是咱們看一下 T.run
和 T.let
,會發現兩個函數是類似的,只有一點不一樣:它們接收參數的方式。下面代碼展現了用兩個函數實現一樣的邏輯:
stringVariable?.run {
println("The length of this String is $length")
}
// Similarly
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
不能被省略的狀況下,例如用做一個函數參數,it
比 this
更短更清晰。
T.let
容許用更好的命名來表示轉換過的所用變量(the converted used variable),也就是說,你能夠把 it
轉換爲其餘名字:
stringVariable?.let {
nonNullString ->
println("The non null string is $nonNullString")
}
複製代碼
如今,咱們看一下 T.let
和 T.also
,若是咱們看一下函數做用域內部的話,會發現二者是同樣的:
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
。
簡單的示例以下:
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
貌似沒什麼意義,由於咱們能夠輕鬆把它們組合進一個單一的函數塊內。仔細想一下,它們會有以下優點:
若是二者結合鏈式來使用,一個進化本身,一個持有本身,就會變得很是強大,例如:
// 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() }
複製代碼
經過這三個特性,咱們能夠清楚地知道每一個函數的行爲。讓咱們舉例說明一下上面沒有提到的 T.apply
函數,它的 3 個特性以下所述:
this
做爲參數this
(它本身)// 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) }
複製代碼
或者咱們也能夠把一個非鏈式的對象建立過程變得可鏈式(chain-able):
// 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) }
複製代碼
如今思路變清晰了,根據這三大特性,咱們能夠對函數進行分類。基於此能夠構建一個決策樹來幫助咱們根據須要來選擇使用哪個函數。
但願上面的決策樹可以更清晰地闡述這些函數,同時也能簡化你的決策,使你可以得當地使用這些函數。