綜述
和Java相比,在Kotlin中提供了不少新的特性。這次我們就來聊一聊Kotlin的一些通用的擴展函數run,with,let,also和apply。對於這五個擴展函數它們都存在於Kotlin的源碼標準庫當中,也就是在Standard.kt文件當中。它們都是適用於任何對象的通用擴展函數。但是對於run,with,let,also和apply這五個函數他們的用法及其相似,以至於我們無法確定去選擇使用哪一個。那麼現在我們就來聊一下這五個函數它們的使用方法,它們的不同之處以及在什麼場景下去使用。
作用域函數
在這裏我們重點是看一下run,with,T.run,T.let,T.also,和T.apply,對於這幾個函數來說它們最重要的功能之一是在調用函數的內部又提供了一個作用域。
那麼下面就通過一段代碼來看一下run函數的作用域,對於其它函數來說當然也是類似。
fun test(){
var animal = "cat"
run {
val animal = "dog"
println(animal) // dog
}
println(animal) //cat
}
在這個簡單的test函數當中我們擁有一個單獨的作用域,在run函數中能夠重新定義一個animal變量,並且它的作用域只存在於run函數當中。
目前對於這個run函數看起來貌似沒有什麼用處,但是在run函數當中它不僅僅只是一個作用域,他還有一個返回值。他會返回在這個作用域當中的最後一個對象。
例如現在有這麼一個場景,用戶領取app的獎勵,如果用戶沒有登錄彈出登錄dialog,如果已經登錄則彈出領取獎勵的dialog。我們可以使用以下代碼來處理這個邏輯。
run {
if (islogin) loginDialog else getAwardDialog
}.show()
可以看到上面這段代碼會變得更加的簡潔,並且可以將show方法一次應用到上面兩個dialog當中,而不是去調用兩次。
with和其它通用擴展函數
在這裏之所以將with函數單獨拿出來進行說明,是因爲with得用法和其它通用的擴展函數的用法比較獨特。在這裏我們依然使用run函數來進行對比。對於下面這段代碼做的是同樣一件事。它們的不同之處就是一個使用了with(T)函數,而另一個則是使用了T.run函數。
with(webView.settings){
javaScriptEnabled = true
databaseEnabled = true
}
webView.settings.run {
javaScriptEnabled = true
databaseEnabled = true
}
但是我們覺得使用哪一個會更好呢?現在假設一種場景,那就是webView.settings可能爲null。那我們就來再次看一下下面這段代碼.
with(webView.settings){
javaScriptEnabled = true
databaseEnabled = true
}
webView.settings?.run {
javaScriptEnabled = true
databaseEnabled = true
}
這麼以來就很明顯了,當然是T.run方法會更好,因爲我們可以在使用這些函數之前可以進行對null的檢查。
對於with也是存在一個返回值,它也是會返回在這個作用域當中的最後一個對象。
作用域中接收者this和it
在這幾個擴展函數當中,它們都能直接獲取到調用的對象或者是with中傳入參數的對象。在這五個擴展函數在它們的作用域中的接收者可以是this或者是it。那麼我們來對比一下T.run和T.let函數。這兩個函數也是十分的相似。
stringVariable?.run {
println("字符串的長度爲$length")
}
stringVariable?.let {
println("字符串的長度爲 ${it.length}")
}
在這兩段代碼中可以清晰的看到。在T.run函數中通過this來獲取stringVariable對象,而在T.let函數中通過it來取出stringVariable對象。當然我們也能夠爲it重新命名。如果我們不想覆蓋外部作用域的this,這時候去使用T.let會更加的方便。至於哪些函數的接收者是this,哪些函數的接收者是it,在後面會通過一張樹狀圖清晰的體現出來。
在作用域中返回值的類型
在這些作用域中它們都會存在一個返回值。在上面的講述的run,with,T.run,T.let中它們返回的都是作用域中最後一個對象。當然它們所返回的值是允許和接受者it或者this對象的類型不同。但是並不是所有的擴展函數都是返回作用域的最後一個對象。例如T.also函數。
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"
}
從上面兩段代碼可以看出T.let和T.also的返回值使不同的。T.let返回的是作用域中的最後一個對象,它的值和類型都可以改變。但是T.also不管調用多少次返回的都是原來的original對象。
對於T.let和T.also都能夠進行鏈式操作,那麼我們現在結合一下T.let和T.also的鏈式調用來看一下在實際場景中的應用。
//原始函數
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() }
擴展函數的特性
到目前爲止除了T.apply沒有使用到以外,根據上面的用法我們可以總結出來這些擴展函數的三大特性。
- 它們都有自己的作用域
- 它們作用域中的接收者是this或者it
- 它們都有一個返回值,返回最後一個對象(this)或者調用者自身(itself)
由此可想到對於T.apply無非也就是這三個特性。對於T.apply它作用域中的接收者是this,並且返回的調用者T。因此,T.apply的其中一個使用場景可以用來創建一個Fragment,代碼如下所示:
// 使用普通的方法創建一個Fragment
fun createInstance(args: Bundle) : MyFragment {
val fragment = MyFragment()
fragment.arguments = args
return fragment
}
// 通過apply來改善原有的方法創建一個Fragment
fun createInstance(args: Bundle)
= MyFragment().apply { arguments = args }
我們也能夠通過T.apply的鏈式調用創建一個Intent:
// 普通創建Intent方法
fun createIntent(intentData: String, intentAction: String): Intent {
val intent = Intent()
intent.action = intentAction
intent.data=Uri.parse(intentData)
return intent
}
// 通過apply函數的鏈式調用創建Intent
fun createIntent(intentData: String, intentAction: String) =
Intent().apply { action = intentAction }
.apply { data = Uri.parse(intentData) }
如何選擇使用
在這裏我們通過一個樹狀圖來看一下對着五個擴展函數的區別,使用以及如何選取擴展函數(圖片來源於參考文獻當中)
總結
在這裏做一下總結,我們可以看出在這五個擴展函數當中它們的特性也是十分的簡單,無非也就是接收者和返回值的不同。對於with,T.run,T.apply接收者是this,而T.let和T.also接受者是it;對於with,T.run,T.let返回值是作用域的最後一個對象(this),而T.apply和T.also返回值是調用者本身(itself)。