Kotlin修煉指南

xuyisheng發表於2019-10-10

Kotlin修煉指南

作用域函式

作用域函式是Kotlin中的一個非常有用的函式,它主要分為兩種,一種是擴充函式式,另一種是頂層函式式。作用域函式的主要功能是為呼叫函式提供一個內部範圍,同時結合kotlin的語法糖提供一些便捷操作。

作用域函式主要有下面這幾種,它們的主要區別就是函式體內使用物件和返回值的區別。

  • run

函式體內使用this代替本物件。返回值為函式最後一行或者return指定的表示式

  • let

函式內使用it代替本物件。返回值為函式最後一行或者return指定的表示式。

  • apply

函式內使用this代替本物件。返回值為本物件。

  • also

函式內使用it代替本物件。返回值為本物件。

  • takeIf

條件為真返回物件本身否則返回null。

  • takeUnless

條件為真返回null否則返回物件本身。

  • with

with比較特殊,不是以擴充套件方法的形式存在的,而是一個頂級函式。傳入引數為物件,函式內使用this代替物件。返回值為函式最後一行或者return指定的表示式。

  • repeat

將函式體執行多次。

通過表格進行下總結,如下所示。

操作符 this/it 返回值
let
it
最後一行或者return指定的表示式
with it
最後一行或者return指定的表示式
run
this
最後一行或者return指定的表示式
also this 上下文物件
apply this 上下文物件

下面通過一個簡單的例子來演示下這些作用域函式的基本使用方式。

class TestBean {
    var name: String = "xuyisheng"
    var age: Int = 18
}
fun main(args: Array<String>) {
    val test = TestBean()
    val resultRun = test.run {
        name = "xys"
        age = 3
        println("Run內部 $this")
        age
    }
    println("run返回值 $resultRun")
    val resultLet = test.let {
        it.name = "xys"
        it.age = 3
        println("let內部 $it")
        it.age
    }
    println("let返回值 $resultLet")
    val resultApply = test.apply {
        name = "xys"
        age = 3
        println("apply內部 $this")
        age
    }
    println("apply返回值 $resultApply")
    val resultAlso = test.also {
        it.name = "xys"
        it.age = 3
        println("also內部 $it")
        it.age
    }
    println("also返回值 $resultAlso")
    val resultWith = with(test) {
        name = "xys"
        age = 3
        println("with內部 $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")
}複製程式碼

執行結果如下所示。

Run內部 TestBean@27c170f0
run返回值 3
let內部 TestBean@27c170f0
let返回值 3
apply內部 TestBean@27c170f0
apply返回值 TestBean@27c170f0
also內部 TestBean@27c170f0
also返回值 TestBean@27c170f0
with內部 TestBean@27c170f0
with返回值 3
takeIf TestBean@27c170f0
takeUnless null複製程式碼

官網提供了一張圖來幫助開發者選擇合適的作用域函式,如下所示。

file

頂級函式使用場景

run、with、repeat,是比較常用的3個頂級函式,它們是區別於其它幾種擴充函式型別的,它們的使用也比較簡單,示例程式碼如下所示。

  • run
fun testRun() {
    var str = "I am xys"
    run {
        val str = "I am zj"
        println(str) // I am xys
    }
    println(str)  // I am zj
}複製程式碼

可以發現,run頂級函式提供了一個獨立的作用域,可以在該作用域內完整的使用全新的變數和屬性。

  • repeat
repeat(5){
    print("repeat")
}複製程式碼

repeat比較簡單,直接將函式體按指定次數執行。

  • with

前面的程式碼已經演示過with如何使用。

with(ArrayList<String>()) {
    add("a")
    add("b")
    add("c")
    println("this = " + this)
    this
}複製程式碼

要注意的是其返回值是根據return的型別或者最後一行程式碼來進行判斷的。

擴充函式使用場景

?.結合擴充函式

Kotlin的?操作符和作用域函式的擴充函式可以非常方便的進行物件的判空及後續處理,例如下面的例子。

// 對result進行了判空並bindData
result?.let {
    if (it.isNotEmpty()) {
        bindData(it)
    }
}複製程式碼

簡化物件的建立

類似apply這樣的作用域函式,可以返回this的作用域函式,可以將物件的建立和屬性的賦值寫在一起,簡化程式碼,類似builder模式,例如下面的這個例子。

// 使用普通的方法建立一個Fragment
fun createInstance(args: Bundle) : MyFragment {
    val fragment = MyFragment()
    fragment.arguments = args
    return fragment
}
// 通過apply來建立一個Fragment
fun createInstance(args: Bundle)
    = MyFragment().apply { arguments = args }複製程式碼

再例如下面的實現。

// 使用普通的方法建立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) }複製程式碼

以及下面的實現。

// 正常方法
fun makeDir(path: String): File  {
    val result = File(path)
    result.mkdirs()
    return result
}
// 改進方法
fun makeDir(path: String) 
    = path.let{ File(it) }.also{ it.mkdirs() }複製程式碼

同一物件的多次操作

在開發中,有些物件有很多引數或者方法需要設定,但該物件又沒用提供builder方式進行構建,例如下面的例子。

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還是其它擴充函式,都可以實現這樣的需求,藉助it或this,可以很方便的對該物件的多個屬性進行操作。

不過這些擴充函式還是有一些細微的差別的,例如T.run和T.let(即使用it和this的區別)

  • 使用it的作用域函式,可以使用特定的變數名來重新命名it,從而表達更清楚的語義。
  • this在大部分情況下是可以省略的,比使用it簡單

例如下面的例子。

stringResult?.let {
      nonNullString ->
      println("The non null string is $nonNullString")
}複製程式碼

通過對it的重新命名,語義表達更加清楚。

條件操作

藉助kotlin的?操作符,可以簡化很多條件操作,例如下面的幾個例子。

url = intent.getStringExtra(EXTRA_URL)?.takeIf { it.isNotEmpty() } ?: run {
    toast("url空")
    activity.finish()
}複製程式碼

上面的程式碼演示了【從intent中取出url並在url為空時的操作】。

test.takeIf { it.name.isNotEmpty() }?.also { print("name is $it.name") } ?: print("name empty")複製程式碼

上面程式碼演示了【從test中取出name,不為空的時候和為空的時候的操作】。

鏈式呼叫

作用域函式的一個非常方便的作用就是通過其返回值的改變來組裝鏈式呼叫。一個簡單示例如下所示。

test.also {
    // todo something
}.apply {
    // todo something
}.name = "xys"複製程式碼

通過let來改變返回值,從而將不同的處理通過鏈式呼叫串聯起來。

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
}複製程式碼

上面的程式碼藉助let,可以將函式的返回值不斷進行修改,從而直接將下一個操作進行鏈式連線。而使用also(即返回this的作用域函式)可以將多個對同一物件的操作進行鏈式呼叫,如下所示。

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"
}複製程式碼

這裡只是為了演示,所以將可以寫在同一個作用域函式中的進行了拆分。

also和let的鏈式呼叫,實際上各有不同的使用技巧,通過let,可以改變返回值,而通過also,可以將多個不同的原子操作通過鏈式進行組合,讓邏輯更加明朗。

國際慣例

also & apply

雖然also和apply都是返回this,但國際慣例,它們在使用的時候,還是有一些細微的差別的,also強調的是【與呼叫者無關的操作】,而apply強調的是【呼叫者的相關操作】,例如下面的這個例子。

test?.also {
    println("some log")
}?.apply {
    name = "xys"
}複製程式碼

let & run

let和run的返回值相同,它們的區別主要在於作用域內使用it和this的區別。一般來說,如果呼叫者的屬性和類中的屬性同名,則一般會使用let,避免出現同名的賦值引起混亂。

國際慣例,run通常使用在鏈式呼叫中,進行資料處理、型別轉換,例如?.run{}的使用。

歡迎關注我的微信公眾號——Android群英傳

相關文章