Kotlin修煉指南(一)
最近會(huì)寫一些關(guān)于kotlin的文章,主要是為了引出函數(shù)式編程的概念,并介紹kotlin中的一些騷操作。第一篇文章主要介紹作用域函數(shù)。
作用域函數(shù)
作用域函數(shù)是Kotlin中的一個(gè)非常有用的函數(shù),它主要分為兩種,一種是拓展函數(shù)式,另一種是頂層函數(shù)式。作用域函數(shù)的主要功能是為調(diào)用函數(shù)提供一個(gè)內(nèi)部范圍,同時(shí)結(jié)合kotlin的語法糖提供一些便捷操作。
作用域函數(shù)主要有下面這幾種,它們的主要區(qū)別就是函數(shù)體內(nèi)使用對象和返回值的區(qū)別。
run
函數(shù)體內(nèi)使用this代替本對象。返回值為函數(shù)最后一行或者return指定的表達(dá)式
let
函數(shù)內(nèi)使用it代替本對象。返回值為函數(shù)最后一行或者return指定的表達(dá)式。
apply
函數(shù)內(nèi)使用this代替本對象。返回值為本對象。
also
函數(shù)內(nèi)使用it代替本對象。返回值為本對象。
takeIf
條件為真返回對象本身否則返回null。
takeUnless
條件為真返回null否則返回對象本身。
with
with比較特殊,不是以擴(kuò)展方法的形式存在的,而是一個(gè)頂級函數(shù)。
傳入?yún)?shù)為對象,函數(shù)內(nèi)使用this代替對象。
返回值為函數(shù)最后一行或者return指定的表達(dá)式。
repeat
將函數(shù)體執(zhí)行多次。
通過表格進(jìn)行下總結(jié),如下所示。
| 操作符 | this/it | 返回值 |
|---|---|---|
| let | it | 最后一行或者return指定的表達(dá)式 |
| with | it | 最后一行或者return指定的表達(dá)式 |
| run | this | 最后一行或者return指定的表達(dá)式 |
| also | this | 上下文對象 |
| apply | this | 上下文對象 |
下面通過一個(gè)簡單的例子來演示下這些作用域函數(shù)的基本使用方式。
class TestBean {
var name: String = "xuyisheng"
var age: Int = 18
}
fun main(args: Array) {
val test = TestBean()
val resultRun = test.run {
name = "xys"
age = 3
println("Run內(nèi)部 $this")
age
}
println("run返回值 $resultRun")
val resultLet = test.let {
it.name = "xys"
it.age = 3
println("let內(nèi)部 $it")
it.age
}
println("let返回值 $resultLet")
val resultApply = test.apply {
name = "xys"
age = 3
println("apply內(nèi)部 $this")
age
}
println("apply返回值 $resultApply")
val resultAlso = test.also {
it.name = "xys"
it.age = 3
println("also內(nèi)部 $it")
it.age
}
println("also返回值 $resultAlso")
val resultWith = with(test) {
name = "xys"
age = 3
println("with內(nèi)部 $this")
age
}
println("with返回值 $resultWith")
test.age = 33
val resultTakeIf = test.takeIf {
it.age > 3
}
println("takeIf $resultTakeIf")
val resultTakeUnless = test.takeUnless {
it.age > 3
}
println("takeUnless $resultTakeUnless")
} 執(zhí)行結(jié)果如下所示。
Run內(nèi)部 TestBean@27c170f0
run返回值 3
let內(nèi)部 TestBean@27c170f0
let返回值 3
apply內(nèi)部 TestBean@27c170f0
apply返回值 TestBean@27c170f0
also內(nèi)部 TestBean@27c170f0
also返回值 TestBean@27c170f0
with內(nèi)部 TestBean@27c170f0
with返回值 3
takeIf TestBean@27c170f0
takeUnless null官網(wǎng)上提供了一張圖來整理這些作用域函數(shù)的使用場景,如下所示。

頂級函數(shù)使用場景
run、with、repeat,是比較常用的3個(gè)頂級函數(shù),它們是區(qū)別于其它幾種拓展函數(shù)類型的,它們的使用也比較簡單,示例代碼如下所示。
run
fun testRun() {
var str = "I am xys"
run {
val str = "I am zj"
println(str) // I am xys
}
println(str) // I am zj
}可以發(fā)現(xiàn),run頂級函數(shù)提供了一個(gè)獨(dú)立的作用域,可以在該作用域內(nèi)完整的使用全新的變量和屬性。
repeat
repeat(5){
print("repeat")
}repeat比較簡單,直接將函數(shù)體按指定次數(shù)執(zhí)行。
with
前面的代碼已經(jīng)演示過with如何使用。
with(ArrayList()) {
add("a")
add("b")
add("c")
println("this = " + this)
this
} 要注意的是其返回值是根據(jù)return的類型或者最后一行代碼來進(jìn)行判斷的。
拓展函數(shù)使用場景
?.結(jié)合拓展函數(shù)
Kotlin的?操作符和作用域函數(shù)的拓展函數(shù)可以非常方便的進(jìn)行對象的判空及后續(xù)處理,例如下面的例子。
// 對result進(jìn)行了判空并bindData
result?.let {
if (it.isNotEmpty()) {
bindData(it)
}
}簡化對象的創(chuàng)建
類似apply這樣的作用域函數(shù),可以返回this的作用域函數(shù),可以將對象的創(chuàng)建和屬性的賦值寫在一起,簡化代碼,類似builder模式,例如下面的這個(gè)例子。
// 使用普通的方法創(chuàng)建一個(gè)Fragment
fun createInstance(args: Bundle) : MyFragment {
val fragment = MyFragment()
fragment.arguments = args
return fragment
}
// 通過apply來創(chuàng)建一個(gè)Fragment
fun createInstance(args: Bundle)
= MyFragment().apply { arguments = args }再例如下面的實(shí)現(xiàn)。
// 使用普通的方法創(chuàng)建Intent
fun createIntent(intentData: String, intentAction: String): Intent {
val intent = Intent()
intent.action = intentAction
intent.data = Uri.parse(intentData)
return intent
}
// 通過apply函數(shù)的鏈?zhǔn)秸{(diào)用創(chuàng)建Intent
fun createIntent(intentData: String, intentAction: String) =
Intent().apply { action = intentAction }
.apply { data = Uri.parse(intentData) }以及下面的實(shí)現(xiàn)。
// 正常方法
fun makeDir(path: String): File {
val result = File(path)
result.mkdirs()
return result
}
// 改進(jìn)方法
fun makeDir(path: String)
= path.let{ File(it) }.also{ it.mkdirs() }同一對象的多次操作
在開發(fā)中,有些對象有很多參數(shù)或者方法需要設(shè)置,但該對象又沒用提供builder方式進(jìn)行構(gòu)建,例如下面的例子。
val linearLayout = LinearLayout(itemView.context).apply {
orientation = LinearLayout.VERTICAL
layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT)
}
progressBar.apply {
progress = newProgress
visibility = if (newProgress in 1..99) View.VISIBLE else View.GONE
}不論是let、run、apply還是其它拓展函數(shù),都可以實(shí)現(xiàn)這樣的需求,借助it或this,可以很方便的對該對象的多個(gè)屬性進(jìn)行操作。
不過這些拓展函數(shù)還是有一些細(xì)微的差別的,例如T.run和T.let(即使用it和this的區(qū)別)
使用it的作用域函數(shù),可以使用特定的變量名來重命名it,從而表達(dá)更清楚的語義。
this在大部分情況下是可以省略的,比使用it簡單
例如下面的例子。
stringResult?.let {
nonNullString ->
println("The non null string is $nonNullString")
}通過對it的重命名,語義表達(dá)更加清楚。
條件操作
借助kotlin的?操作符,可以簡化很多條件操作,例如下面的幾個(gè)例子。
url = intent.getStringExtra(EXTRA_URL)?.takeIf { it.isNotEmpty() } ?: run {
toast("url空")
activity.finish()
}上面的代碼演示了【從intent中取出url并在url為空時(shí)的操作】。
test.takeIf { it.name.isNotEmpty() }?.also { print("name is $it.name") } ?: print("name empty")上面代碼演示了【從test中取出name,不為空的時(shí)候和為空的時(shí)候的操作】。
鏈?zhǔn)秸{(diào)用
作用域函數(shù)的一個(gè)非常方便的作用就是通過其返回值的改變來組裝鏈?zhǔn)秸{(diào)用。一個(gè)簡單示例如下所示。
test.also {
// todo something
}.apply {
// todo something
}.name = "xys"通過let來改變返回值,從而將不同的處理通過鏈?zhǔn)秸{(diào)用串聯(lián)起來。
val original = "abc"
// 改變值并且傳遞到下一鏈條
original.let {
println("The original String is $it") // "abc"
it.reversed() // 改變參數(shù)并且傳遞到下一鏈條
}.let {
println("The reverse String is $it") // "cba"
it.length // 改變參數(shù)類型并且傳遞到下一鏈條
}.let {
println("The length of the String is $it") // 3
}上面的代碼借助let,可以將函數(shù)的返回值不斷進(jìn)行修改,從而直接將下一個(gè)操作進(jìn)行鏈?zhǔn)竭B接。
而使用also(即返回this的作用域函數(shù))可以將多個(gè)對同一對象的操作進(jìn)行鏈?zhǔn)秸{(diào)用,如下所示。
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"
}這里只是為了演示,所以將可以寫在同一個(gè)作用域函數(shù)中的進(jìn)行了拆分。
also和let的鏈?zhǔn)秸{(diào)用,實(shí)際上各有不同的使用技巧,通過let,可以改變返回值,而通過also,可以將多個(gè)不同的原子操作通過鏈?zhǔn)竭M(jìn)行組合,讓邏輯更加明朗。
國際慣例
also & apply
雖然also和apply都是返回this,但國際慣例,它們在使用的時(shí)候,還是有一些細(xì)微的差別的,also強(qiáng)調(diào)的是【與調(diào)用者無關(guān)的操作】,而apply強(qiáng)調(diào)的是【調(diào)用者的相關(guān)操作】,例如下面的這個(gè)例子。
test?.also {
println("some log")
}?.apply {
name = "xys"
}let & run
let和run的返回值相同,它們的區(qū)別主要在于作用域內(nèi)使用it和this的區(qū)別。一般來說,如果調(diào)用者的屬性和類中的屬性同名,則一般會(huì)使用let,避免出現(xiàn)同名的賦值引起混亂。
國際慣例,run通常使用在鏈?zhǔn)秸{(diào)用中,進(jìn)行數(shù)據(jù)處理、類型轉(zhuǎn)換,例如?.run{}的使用。
