翻譯說明:java
原標題: Mastering Kotlin standard functions: run, with, let, also and applyweb
原文地址: medium.com/@elye.proje…app
原文做者: Elyeless
Kotlin中的一些標準庫函數很是類似,以至於咱們不肯定要使用哪一個函數。這裏我將介紹一種簡單的方法來清楚地區分它們之間的差別以及如何選擇使用哪一個函數。函數
下面我將關於 run、with、T.run、T.let、T.also 和 T.apply 這些函數,並把它們稱爲做用域函數,由於我注意到它們的主要功能是爲調用者函數提供內部做用域。學習
說明做用域最簡單的方式是 run 函數this
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的操做實現。編碼
這個做用域函數自己彷佛看起來不是頗有用。可是這還有一個比做用域有趣一點是,它返回一些東西,是這個做用域內部的最後一個對象。spa
所以,如下的內容會變得更加整潔,咱們能夠將show()方法應用到兩個View中,而不須要去調用兩次show()方法。翻譯
run {
if (firstTimeView) introView else normalView
}.show()
複製代碼
爲了讓做用域函數更有趣,讓我把他們的行爲分類成三個屬性特徵。我將會使用這些屬性特徵來區分他們每個函數。
若是咱們對比 with 和 T.run 這兩個函數的話,他們其實是十分類似的。下面使用他們實現相同的功能的例子.
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.lenght}. 因此我把這個稱之爲傳遞 this參數
然而對於 T.let 函數的聲明,你將會注意到 T.let 是傳遞它本身自己到函數中block: (T)。所以這個相似於傳遞一個lambda表達式做爲參數。它能夠在函數做用域內部使用it來指代. 因此我把這個稱之爲傳遞 it參數
從上面看,彷佛T.run比T.let更加優越,由於它更隱含,可是T.let函數具備一些微妙的優點,以下所示:
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類型自己,即這個。
這兩個函數對於函數的鏈式調用都頗有用,其中T.let讓您演變操做,而T.also則讓您對相同的變量執行操做。
簡單的例子以下:
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() }
複製代碼
經過回顧這3個屬性特徵,咱們能夠很是清楚函數的行爲。讓我來講明T.apply函數,因爲我並無以上函數中提到過它。 T.apply的三個屬性以下
所以,使用它,能夠想象下它能夠被用做:
// 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 }
複製代碼
或者咱們也可讓無鏈對象建立鏈式調用。
// 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) }
複製代碼
所以,顯然有了這3個屬性特徵,咱們如今能夠對功能進行相應的分類。基於此,咱們能夠在下面構建一個決策樹,以幫助肯定咱們想要使用哪一個函數,來選擇咱們須要的。
但願上面的決策樹可以更清晰地說明功能,並簡化你的決策,使你可以適當掌握這些功能的使用.
咱們都知道在Kotlin中Standard.Kt文件中短短不到100來行庫函數源碼,可是它們做用是很是強大,能夠說它們是貫穿於整個Kotlin開發編碼過程當中。使用它們能讓你的代碼會更具備可讀性、更優雅、更簡潔。善於合理使用標準庫函數,也是衡量你對Kotlin掌握程度標準之一,由於你去看一些開源Kotlin源碼,隨處可見的都是使用各類標準庫函數。
可是這些庫函數有難點在於它們的用法都很是類似,有的人甚至認爲有的庫函數都是多餘的,其實否則,每一個庫函數都是有它的實際應用場景。雖然有時候你能用一種庫函數也能實現相同的功能,可是也許那並非最好的實現方式。相信不少初學者對於這些標準庫函數也是傻傻分不清楚(曾經的我也是),可是這篇博客很是一點在於它提取出了這些庫函數三個主要特徵:是不是擴展函數、是否傳遞this或it作爲參數(在函數內部表現就是this和it的指代)、是否須要返回調用者對象自己,基於特徵就能夠進行分類,分類後相應的應用場景也就一目瞭然。這種善於提取特徵思路仍是值得學習的。
第一點: 建議儘可能不要使用多個標準庫函數進行嵌套,不要爲了簡化而去作簡化,不然整個代碼可讀性會大大下降,一會是it指代,一會又是this指代,估計隔一段時間後連你本身都不知道指代什麼了。
第二點: 針對上面譯文的let函數和run函數須要補充下,他們之因此可以返回其餘類型的值,其原理在於內部block lambda表達式返回的R類型,也就是這二者函數的返回值類型取決於傳入block lambda表達式返回類型,然而決定block lambda表達式返回值類型,取決於外部傳入lambda表達式體內最後一行返回值
第三點: 關於T.also和T.apply函數爲何都能返回本身自己,是由於在各自Lambda表達式內部最後一行都調用return this,返回它們本身自己,這個this能被指代調用者,是由於它們都是擴展函數特性
關於標準庫函數本篇譯文在於告知應用場景以及理清它們的區別以及在使用庫函數簡化代碼實現時要掌握好度,不要濫用不然你的代碼可讀性會不好,後續會深刻每一個標準庫函數內部原理作分析,歡迎關注。
歡迎關注Kotlin開發者聯盟,這裏有最新Kotlin技術文章,每週會不按期翻譯一篇Kotlin國外技術文章。若是你也喜歡Kotlin,歡迎加入咱們~~~