翻譯說明:
原標題: Mastering Kotlin standard functions: run, with, let, also and apply
原文地址: medium.com/@elye.proje…
原文作者: Elye
Kotlin中的一些標準庫函式非常相似,以致於我們不確定要使用哪個函式。這裡我將介紹一種簡單的方法來清楚地區分它們之間的差異以及如何選擇使用哪個函式。
作用域函式
下面我將關於 run、with、T.run、T.let、T.also 和 T.apply 這些函式,並把它們稱為作用域函式,因為我注意到它們的主要功能是為呼叫者函式提供內部作用域。
說明作用域最簡單的方式是 run 函式
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的操作實現。
這個作用域函式本身似乎看起來不是很有用。但是這還有一個比作用域有趣一點是,它返回一些東西,是這個作用域內部的最後一個物件。
因此,以下的內容會變得更加整潔,我們可以將show()方法應用到兩個View中,而不需要去呼叫兩次show()方法。
run {
if (firstTimeView) introView else normalView
}.show()
複製程式碼
作用域函式的三個屬性特徵
為了讓作用域函式更有趣,讓我把他們的行為分類成三個屬性特徵。我將會使用這些屬性特徵來區分他們每一個函式。
1、普通函式 VS 擴充套件函式 (Normal vs. extension function)
如果我們對比 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 擴充套件函式更好,因為我們可以在使用它之前對可空性進行檢查。
2、this VS it 引數(This vs. it argument)
如果我們對比 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函式具有一些微妙的優勢,如下所示:
- 1、T.let函式提供了一種更清晰的區分方式去使用給定的變數函式/成員與外部類函式/成員。
- 2、例如當it作為函式的引數傳遞時,this不能被省略,並且it寫起來比this更簡潔,更清晰。
- 3、T.let允許更好地命名已轉換的已使用變數,即可以將it轉換為其他有含義名稱,而 T.run則不能,內部只能用this指代或者省略。
stringVariable?.let {
nonNullString ->
println("The non null string is $nonNullString")
}
複製程式碼
3、返回this VS 其他型別 (Return this vs. other type)
現在,讓我們看看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似乎看上去沒有意義,因為我們可以很容易地將它們組合成一個功能塊。仔細思考,它有一些很好的優點。
- 1、它可以對相同的物件提供非常清晰的分離過程,即建立更小的函式部分。
- 2、在使用之前,它可以非常強大的進行自我操作,從而實現整個鏈式程式碼的構建操作。
當兩者結合在一起使用時,即一個自身演變,一個自我保留,它能使一些操作變得更加強大。
// 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的三個屬性如下
- 1、它是一個擴充套件函式
- 2、它是傳遞this作為引數
- 3、它是返回 this (即它自己本身)
因此,使用它,可以想象下它可以被用作:
// 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個屬性特徵,我們現在可以對功能進行相應的分類。基於此,我們可以在下面構建一個決策樹,以幫助確定我們想要使用哪個函式,來選擇我們需要的。
希望上面的決策樹能夠更清晰地說明功能,並簡化你的決策,使你能夠適當掌握這些功能的使用.
譯者有話說
- 1、為什麼我要翻譯這篇部落格?
我們都知道在Kotlin中Standard.Kt檔案中短短不到100來行庫函式原始碼,但是它們作用是非常強大,可以說它們是貫穿於整個Kotlin開發編碼過程中。使用它們能讓你的程式碼會更具有可讀性、更優雅、更簡潔。善於合理使用標準庫函式,也是衡量你對Kotlin掌握程度標準之一,因為你去看一些開源Kotlin原始碼,隨處可見的都是使用各種標準庫函式。
但是這些庫函式有難點在於它們的用法都非常相似,有的人甚至認為有的庫函式都是多餘的,其實不然,每個庫函式都是有它的實際應用場景。雖然有時候你能用一種庫函式也能實現相同的功能,但是也許那並不是最好的實現方式。相信很多初學者對於這些標準庫函式也是傻傻分不清楚(曾經的我也是),但是這篇部落格非常一點在於它提取出了這些庫函式三個主要特徵:是否是擴充套件函式、是否傳遞this或it做為引數(在函式內部表現就是this和it的指代)、是否需要返回撥用者物件本身,基於特徵就可以進行分類,分類後相應的應用場景也就一目瞭然。這種善於提取特徵思路還是值得學習的。
- 2、關於使用標準庫函式需要補充的幾點。
第一點: 建議儘量不要使用多個標準庫函式進行巢狀,不要為了簡化而去做簡化,否則整個程式碼可讀性會大大降低,一會是it指代,一會又是this指代,估計隔一段時間後連你自己都不知道指代什麼了。
第二點: 針對上面譯文的let函式和run函式需要補充下,他們之所以能夠返回其他型別的值,其原理在於內部block lambda表示式返回的R型別,也就是這兩者函式的返回值型別取決於傳入block lambda表示式返回型別,然而決定block lambda表示式返回值型別,取決於外部傳入lambda表示式體內最後一行返回值
第三點: 關於T.also和T.apply函式為什麼都能返回自己本身,是因為在各自Lambda表示式內部最後一行都呼叫return this,返回它們自己本身,這個this能被指代呼叫者,是因為它們都是擴充套件函式特性
- 3、總結
關於標準庫函式本篇譯文在於告知應用場景以及理清它們的區別以及在使用庫函式簡化程式碼實現時要掌握好度,不要濫用否則你的程式碼可讀性會很差,後續會深入每個標準庫函式內部原理做分析,歡迎關注。
歡迎關注Kotlin開發者聯盟,這裡有最新Kotlin技術文章,每週會不定期翻譯一篇Kotlin國外技術文章。如果你也喜歡Kotlin,歡迎加入我們~~~