Kotlin - 作用域函數(shù)

什么是作用域函數(shù)(Scope Functions)?

Kotlin 標(biāo)準(zhǔn)庫包含了幾個(gè)特殊的函數(shù),其目的是在調(diào)用對(duì)象的上下文環(huán)境(context)中執(zhí)行代碼塊。當(dāng)你在提供了 lambda 表達(dá)式的對(duì)象上調(diào)用此類函數(shù)時(shí),它會(huì)形成一個(gè)臨時(shí)作用域。在此作用域內(nèi),你可以在不使用其名稱的情況下訪問該對(duì)象,這些函數(shù)被稱為作用域函數(shù)。在 Kotlin 中,作用域函數(shù)總共有五個(gè),分別是:let、run、with、applyalso。接下來我們逐個(gè)詳細(xì)分析。

開始分析之前,你可能需要簡單了解下它大概長什么樣,下面是個(gè)簡單示例

data class Person(var name:String){
    fun say(words:String){
        println("$name says $words")
    }
}

fun main() {
    Person("skyrin").let{
        it.say("hello")
        println(it)
    }
}

如果不使用 let 的話,你需要先創(chuàng)建出對(duì)象,然后再執(zhí)行調(diào)用

val person = Person("skyrin")
person.say("hello")
println(person)

所以,作用域函數(shù)的目的就是盡可能的讓你的代碼變得更簡潔更具可讀性,盡可能少的創(chuàng)建對(duì)象,僅此而已。

由于這 5 個(gè)作用域函數(shù)的性質(zhì)有些相似,所以大家可能經(jīng)常不知道在哪種情況下該使用哪個(gè)函數(shù),以至于最終放棄使用作用域函數(shù),所以為了避免類似悲劇發(fā)生,我們首先來討論一下他們之間的區(qū)別以及使用場景。

區(qū)別

由于作用域函數(shù)本質(zhì)上非常相似,因此理解它們之間的差異非常重要。每個(gè)作用域函數(shù)有兩個(gè)主要區(qū)別:

  • 引用上下文對(duì)象的方式
  • 返回值
區(qū)別1:上下文對(duì)象(Context)是 this 還是 it
this

run、withapply 通過 this 關(guān)鍵字引用一個(gè) context 對(duì)象作為 lambda 接收者。于是,在他們的 lambda 中,this 對(duì)象可用于普通類函數(shù)中。大多數(shù)情況下,在訪問接收者的成員時(shí),可以省略 this 關(guān)鍵字,讓代碼保持簡潔。另一方面,如果省略了 this ,你就很難區(qū)分你操作的函數(shù)或變量是外部對(duì)象的還是接收者的了,所以,context 對(duì)象作為一個(gè)接收者(this)這種方式推薦用于調(diào)用接收者(this) 的成員變量或函數(shù)。示例如下

data class Person(var name: String,var age: Int = 0,var city: String = "")
fun main() {
    val person = Person("Skyrin").apply {
        age = 18    // 等價(jià)于 this.age = 18 或閉包外部的 person.age = 18
        city = "Beijing"
    }
    // 如上寫法可替代如下寫法
    // person.age = 18
    // person.city = "Beijing"
    println(person)
}
it

letalso 有一個(gè)作為 lambda 參數(shù)傳入的 context 對(duì)象,如果不指定參數(shù)名,則可以通過該 context 對(duì)象的隱式默認(rèn)名稱 it 來訪問它,itthis 看上去更簡潔,用于表達(dá)式中也會(huì)使代碼更加清晰易讀。但是,當(dāng)你訪問 context 對(duì)象的函數(shù)或者屬性時(shí),不能像 apply 那樣省略 this ,因此,當(dāng) context 對(duì)象主要用作參數(shù)被其他函數(shù)調(diào)用時(shí),用 it 更好一些。

import kotlin.random.Random
fun writeToLog(message: String) {
    println("INFO: $message")
}
fun getRandomInt(): Int {
    return Random.nextInt(100).also {
        writeToLog("getRandomInt() generated value $it")
    }
 }
fun main() {
    val i = getRandomInt()
}

你也可以為 context 對(duì)象指定任意參數(shù)名

import kotlin.random.Random
fun writeToLog(message: String) {
    println("INFO: $message")
}
fun getRandomInt(): Int {
    return Random.nextInt(100).also { value -> // use value replace it
        writeToLog("getRandomInt() generated value $value")
    }
}
fun main() {
    val i = getRandomInt()
}
區(qū)別2:返回值是 Context 對(duì)象還是 Lambda 的結(jié)果

作用域函數(shù)的返回值不同:

  • applayalso 返回 context 對(duì)象
  • let、runwith 返回閉包的運(yùn)算結(jié)果
返回 Context 對(duì)象

applayalso 返回 context 對(duì)象,因此,它們可以結(jié)合起來進(jìn)行鏈?zhǔn)秸{(diào)用

fun main() {
    val memberList = mutableListOf<Int>()
    memberList.also {
        println("填充 $it")
    }.apply {
        add(35)
        add(98)
        add(1)
        add(18)
    }.also {
        println("排序并打印 $it")
    }.also {
        it.sort()
        println(it)
    }
}

也可以在 return 語句中使用,將 context 對(duì)象作為函數(shù)的返回值

import kotlin.random.Random
fun main() {
    fun getRandomInt(): Int {
        return Random.nextInt(100).also { value ->
            writeToLog("getRandomInt() generated value $value")
        }
    }
    val i = getRandomInt()
}
fun writeToLog(message: String) {
    println("INFO: $message")
}
返回 Lambda 閉包結(jié)果

letrun、with 返回 lambda 閉包結(jié)果。所以,你可以將其執(zhí)行結(jié)果賦值給任意變量

fun main() {
    val numbers = mutableListOf(1, 3, 5, 6, 7, 9)
    val biggerThan6 = numbers.run {
        add(10)
        add(12)
        filter { it > 6 }
    }
    println("The result of bigger than 6 is $biggerThan6")
}

此外,你可以忽略返回值,使用 with 作用域函數(shù)來為變量創(chuàng)建一個(gè)臨時(shí)作用域

fun main() {
    val numbers = mutableListOf(1, 3, 5, 6, 7, 9)
    with(numbers){
        val first = first()
        val last = last()
        println("first item is $first and last item is $last")
    }
}

使用場景

下面介紹如何適當(dāng)?shù)倪x擇作用域函數(shù),從技術(shù)上來說,它們的功能在很多情況下都是可以互相轉(zhuǎn)換的,所以下面的例子只是展示了一種通用做法,具體選擇還是要看你的業(yè)務(wù)場景更適合哪種情況。

let

context 對(duì)象作為閉包參數(shù)(it)傳入,返回值是閉包結(jié)果。

let 可用于在調(diào)用鏈的結(jié)果上調(diào)用一個(gè)或多個(gè)函數(shù)。例如,以下代碼打印集合上的兩個(gè)操作的結(jié)果

fun main() {
    val numbers = mutableListOf("one", "two", "three", "four", "five")
    val resultList = numbers.map { it.length }.filter { it > 3 }
    println(resultList)
}

使用 let 可以重寫為

fun main() {
    val numbers = mutableListOf("one", "two", "three", "four", "five")
    numbers.map { it.length }.filter { it > 3}.let {
        println(it)
        // 執(zhí)行更多方法調(diào)用
    }
}

如果閉包模塊只有一個(gè)函數(shù)將 context 作為參數(shù)傳入,你可以使用(::)替換 lambda

fun main() {
    val numbers = mutableListOf("one", "two", "three", "four", "five")
    numbers.map { it.length }.filter { it > 3}.let(::print)
}

let 也經(jīng)常被用于執(zhí)行閉包代碼塊中使用非空值的函數(shù),要對(duì)非空對(duì)象執(zhí)行操作,使用安全調(diào)用操作符 ?. 后跟 let 閉包,在此閉包中,原來的可空對(duì)象就可以被轉(zhuǎn)換為非空對(duì)象執(zhí)行操作

fun processNonNullString(str: String) {
    println(str.length)
}
fun main() {
    val str: String? = "Hello"
//    processNonNullString(str)       // 編譯錯(cuò)誤: str 為可空對(duì)象,要求參數(shù)為不可空對(duì)象
    val length = str?.let {
        println("let() called on $it")
        processNonNullString(it)      // 正常執(zhí)行: 'it' 在 '?.let { }' 中為不可空對(duì)象
        it.length
    }
    println("result for let is $length")
}

let 的另一種使用場景是引入局部變量,限制其作用域范圍,以提高代碼可讀性。

fun main() {
    val numbers = listOf("one", "two", "three", "four")
    val modifiedFirstItem = numbers.first().let { firstItem ->
        println("The first item of the list is '$firstItem'")
        if (firstItem.length >= 5) firstItem else "!$firstItem!"
    }.toUpperCase()
    println("First item after modifications: '$modifiedFirstItem'")
}
with

非拓展函數(shù)。context 對(duì)象作為參數(shù)傳遞,但在 lambda 內(nèi)部,它可用作接收器(this),返回值為 lambda 結(jié)果

官方建議是使用 context 對(duì)象調(diào)用函數(shù)而不提供 lambda 結(jié)果。在代碼中,你可以簡單的把 with 函數(shù)理解為 “使用此對(duì)象,執(zhí)行以下操作”

fun main() {
    val numbers = mutableListOf("one", "two", "three")
    with(numbers) { // 使用 numbers 對(duì)象,執(zhí)行 {} 中的操作 
        println("'with' is called with argument $this")
        println("It contains $size elements")
    }
}

with 的另一個(gè)用例是引入一個(gè)輔助對(duì)象,我們可以方便的使用此對(duì)象的屬性或函數(shù)來計(jì)算值

fun main() {
    val numbers = mutableListOf("one", "two", "three")
    val firstAndLast = with(numbers) {
        "The first element is ${first()}," +
                " the last element is ${last()}"
    }
    println(firstAndLast)
}
run

context 對(duì)象可用作接收器(this),返回值為 lambda 結(jié)果

runwith 的作用類似,但是調(diào)用方法和 let 一樣 —— 作為 context 對(duì)象的拓展函數(shù)

當(dāng)你的 lambda 同時(shí)包含了對(duì)象初始化和返回值計(jì)算時(shí),run 函數(shù)非常適合

lass MultiportService(var url: String, var port: Int) {
    fun prepareRequest(): String = "Default request"
    fun query(request: String): String = "Result for query '$request'"
}

fun main() {
    val service = MultiportService("https://example.kotlinlang.org", 80)

    val result = service.run {
        port = 8080
        query(prepareRequest() + " to port $port")
    }
  
    // 同樣的代碼使用 let() 函數(shù)重寫:
    val letResult = service.let {
        it.port = 8080
        it.query(it.prepareRequest() + " to port ${it.port}")
    }
    println(result)
    println(letResult)
}

除了在接收器對(duì)象上調(diào)用run之外,還可以將其用作非擴(kuò)展函數(shù)。非擴(kuò)展 run 允許你執(zhí)行需要表達(dá)式的多個(gè)語句塊。

fun main() {
    val hexNumberRegex = run {
        val digits = "0-9"
        val hexDigits = "A-Fa-f"
        val sign = "+-"
        Regex("[$sign]?[$digits$hexDigits]+")
    }
    for (match in hexNumberRegex.findAll("+1234 -FFFF not-a-number")) {
        println(match.value)
    }
}
apply

context 對(duì)象可用作接收器(this),返回調(diào)用者本身

使用apply不會(huì)返回代碼塊的值,主要對(duì)接收器對(duì)象的成員進(jìn)行操作。 apply的常見用法是對(duì)象配置。此類調(diào)用可以看作“將以下賦值應(yīng)用于對(duì)象”。

data class Person(var name: String,var age: Int = 0,var city: String = "")
fun main() {
    val person = Person("Skyrin").apply {
        age = 18
        city = "Beijing"
    }
}

將接收器作為返回值,你可以輕松進(jìn)行鏈?zhǔn)秸{(diào)用以處理更復(fù)雜的操作。

also

context 對(duì)象作為參數(shù)傳入,返回調(diào)用者本身

also 適用于執(zhí)行將 context 對(duì)象作為參數(shù)進(jìn)行的一些操作。還可用于不更改對(duì)象的其他操作,例如記錄或打印調(diào)試信息。通常,你可以在不破壞程序邏輯的情況下從調(diào)用鏈中刪除 also 調(diào)用。

fun main() {
    val numbers = mutableListOf("one", "two", "three")
    numbers
        .also { println("The list elements before adding new one: $it") }
        .add("four")
}

函數(shù)選擇

以下是它們之間的差異表,以幫助你選擇合適的作用域函數(shù)

函數(shù) 對(duì)象引用 返回值 擴(kuò)展函數(shù)
let it lambda 結(jié)果
run this lambda 結(jié)果
run - lambda 結(jié)果 否:無 context 對(duì)象
with this lambda 結(jié)果 否:將 context 對(duì)象作為參數(shù)
apply this 調(diào)用者本身(context)
also it 調(diào)用者本身(context)

以下是根據(jù)預(yù)期目的選擇范圍功能的簡短指南:

  • 在非 null 對(duì)象上執(zhí)行 lambda:let
  • 將表達(dá)式作為局部范圍中的變量引入:let
  • 對(duì)象配置:apply
  • 對(duì)象配置并計(jì)算結(jié)果:run
  • 運(yùn)行需要表達(dá)式的語句:非擴(kuò)展 run
  • 附加效果:also
  • 對(duì)函數(shù)進(jìn)行分組調(diào)用:with

takeIf 和 takeUnless

除了作用域函數(shù)之外,標(biāo)準(zhǔn)庫還包含函數(shù) takeIf 和 takeUnless。這些函數(shù)允許你在調(diào)用鏈中嵌入對(duì)象狀態(tài)的檢查。

這兩個(gè)函數(shù)的作用是對(duì)象過濾器,takeIf 返回滿足條件的對(duì)象或 null。takeUnless 則剛好相反,它返回不滿足條件的對(duì)象或 null。過濾條件位于函數(shù)的 {} 中。

import kotlin.random.*

fun main() {
    val number = Random.nextInt(100)

    val evenOrNull = number.takeIf { it % 2 == 0 }
    val oddOrNull = number.takeUnless { it % 2 == 0 }
    println("偶數(shù): $evenOrNull, 奇數(shù): $oddOrNull")
}

在 takeIf 和 takeUnless 之后鏈接其他函數(shù)時(shí),不要忘記執(zhí)行空檢查或安全調(diào)用(?.),因?yàn)樗鼈兊姆祷刂凳强煽盏摹?/p>

fun main() {
    val str = "Hello"
    val caps = str.takeIf { it.isNotEmpty() }?.toUpperCase()
    //val caps = str.takeIf { it.isNotEmpty() }.toUpperCase() // 編譯出錯(cuò)
    println(caps)
}

takeIf 和 takeUnless 與作用域函數(shù)一起使用特別有用。一個(gè)很好的例子是使用 let 來鏈接它們,以便在與給定條件匹配的對(duì)象上運(yùn)行代碼塊。

fun main() {
    fun displaySubstringPosition(input: String, sub: String) {
        input.indexOf(sub).takeIf { it >= 0 }?.let {
            println("The substring $sub is found in $input.")
            println("Its start position is $it.")
        }
    }

    displaySubstringPosition("010000011", "11")
    displaySubstringPosition("010000011", "12")
}

總結(jié)

以上,就是所有作用域函數(shù)的功能及使用場景的介紹,你可能已經(jīng)發(fā)現(xiàn),這其中有幾個(gè)函數(shù)的功能相似甚至重疊,有人甚至覺得有這個(gè)時(shí)間去弄明白它們,我早就用其它常規(guī)方式實(shí)現(xiàn)功能了,但有人就覺得這些函數(shù)非常簡潔實(shí)用,用過就再也回不去了。我覺得這就是 Kotlin 的一種優(yōu)點(diǎn)和缺點(diǎn)的體現(xiàn),優(yōu)點(diǎn)是它很靈活,靈活的不像 Native 語言,缺點(diǎn)是它太靈活了,太多的語法糖導(dǎo)致你容易忘記寫這些代碼要實(shí)現(xiàn)的目的,所以,雖然作用域函數(shù)是使代碼更簡潔的一種方法,但還是要避免過度使用它們。

Reference

https://kotlinlang.org/docs/reference/scope-functions.html

附:作用域函數(shù)適用場景圖

1_cKwEowUXup3K7LmiMgn3XQ.png
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 前言 最近使用kotlin語言開發(fā)了新的項(xiàng)目,kotlin的一些特性和大量的語法糖相當(dāng)好用,相比于java,開發(fā)效...
    SirWwh閱讀 2,411評(píng)論 1 2
  • 上面是常用的五個(gè)作用域函數(shù) run let with apply also從定義上我們看出apply 和 also...
    莫庫施勒閱讀 592評(píng)論 0 0
  • 函數(shù)和對(duì)象 1、函數(shù) 1.1 函數(shù)概述 函數(shù)對(duì)于任何一門語言來說都是核心的概念。通過函數(shù)可以封裝任意多條語句,而且...
    道無虛閱讀 4,926評(píng)論 0 5
  • 本文是在學(xué)習(xí)和使用kotlin時(shí)的一些總結(jié)與體會(huì),一些代碼示例來自于網(wǎng)絡(luò)或Kotlin官方文檔,持續(xù)更新... 對(duì)...
    竹塵居士閱讀 3,461評(píng)論 0 8
  • 寫在開頭:本人打算開始寫一個(gè)Kotlin系列的教程,一是使自己記憶和理解的更加深刻,二是可以分享給同樣想學(xué)習(xí)Kot...
    胡奚冰閱讀 1,408評(píng)論 0 6

友情鏈接更多精彩內(nèi)容