kotlin作用域函數(shù):run、let、also、apply、with

剛開(kāi)始學(xué)習(xí) kotlin 的時(shí)候,對(duì)于這些作用域函數(shù)一頭霧水,搞不明白為什么要弄出來(lái)這么多東西。現(xiàn)在來(lái)看看他們具體的區(qū)別以及適用的場(chǎng)景。 Kotlin 標(biāo)準(zhǔn)庫(kù)包含幾個(gè)函數(shù),它們的唯一目的是在對(duì)象的上下文中執(zhí)行代碼塊。 當(dāng)對(duì)一個(gè)對(duì)象調(diào)用這樣的函數(shù)并提供一個(gè)lambda表達(dá)式時(shí),它會(huì)形成一個(gè)臨時(shí)作用域。在此作用域中,可以訪問(wèn)該對(duì)象而無(wú)需其名稱(chēng)。這些函數(shù)稱(chēng)為作用域函數(shù)。 共有以下五種:letrunwithapply以及also。 廢話不多說(shuō),先把從 kotlin 官方上扒拉下來(lái)的結(jié)論放這里

作用域函數(shù)中文版
作用域函數(shù)英文版

總結(jié)在前面

文章太長(zhǎng)太啰嗦,直接看這里的結(jié)論:

函數(shù) 對(duì)象引用 返回值 是否是擴(kuò)展函數(shù)
let it Lambda表達(dá)式結(jié)果
run this Lambda表達(dá)式結(jié)果
run - Lambda表達(dá)式結(jié)果 不是:調(diào)用無(wú)需上下文對(duì)象
with this Lambda表達(dá)式結(jié)果 不是:把上下文對(duì)象當(dāng)做參數(shù)
apply this 上下文對(duì)象
also it 上下文對(duì)象

以下是根據(jù)預(yù)期目的選擇作用域函數(shù)的簡(jiǎn)短指南:

  • 對(duì)一個(gè)非空(non-null)對(duì)象執(zhí)行 lambda 表達(dá)式:let
  • 將表達(dá)式作為變量引入為局部作用域中:let
  • 對(duì)象配置:apply
  • 對(duì)象配置并且計(jì)算結(jié)果:run
  • 在需要表達(dá)式的地方運(yùn)行語(yǔ)句:非擴(kuò)展的 run
  • 附加效果:also
  • 一個(gè)對(duì)象的一組函數(shù)調(diào)用:with 不同作用域函數(shù)的使用場(chǎng)景存在重疊,可以根據(jù)項(xiàng)目或團(tuán)隊(duì)中使用的特定約定來(lái)選擇使用哪些函數(shù)。

雖然作用域函數(shù)可以讓代碼更加簡(jiǎn)潔,但是要避免過(guò)度使用它們:這會(huì)使代碼難以閱讀并可能導(dǎo)致錯(cuò)誤。 我們還建議避免嵌套作用域函數(shù),同時(shí)鏈?zhǔn)秸{(diào)用它們時(shí)要小心:因?yàn)楹苋菀谆煜?dāng)前上下文對(duì)象與thisit的值。

使用示例

假如我們有這么一個(gè)數(shù)據(jù)類(lèi)

data class Book(var name: String, var price: Int) {
    fun changePrice(price: Int) {
        this.price = price
    }
}
val book = Book("book name", 68)

函數(shù)聲明


public inline fun <T> T.also(block: (T) -> Unit): T
public inline fun <T> T.apply(block: T.() -> Unit): T 

public inline fun <T, R> T.let(block: (T) -> R): R
public inline fun <T, R> T.run(block: T.() -> R): R

public inline fun <R> run(block: () -> R): R

public inline fun <T, R> with(receiver: T, block: T.() -> R): R

我們把看起來(lái)相近的作用域函數(shù)的聲明放在一塊對(duì)比著看,看到這里就清楚了的就不要往下看了,看了也是浪費(fèi)時(shí)間。

also

函數(shù)聲明

public inline fun <T> T.also(block: (T) -> Unit): T

also函數(shù)是對(duì)泛型 T 的擴(kuò)展函數(shù),接收一個(gè)參數(shù)類(lèi)型為T(mén)、無(wú)返回值(返回值為Unit類(lèi)型)的函數(shù),且also函數(shù)的返回值就是調(diào)用者。

  • 上下文對(duì)象作為 lambda 表達(dá)式的參數(shù)(it)來(lái)訪問(wèn)。
  • 返回值是上下文對(duì)象本身。

對(duì)于執(zhí)行一些將上下文對(duì)象作為參數(shù)的操作很有用。 對(duì)于需要引用對(duì)象而不是其屬性與函數(shù)的操作,或者不想屏蔽來(lái)自外部作用域的 this 引用時(shí),請(qǐng)使用 also。 當(dāng)你在代碼中看到 also 時(shí),可以將其理解為并且用該對(duì)象執(zhí)行以下操作

val alsoResult = book.also {
    it.changePrice(20)
    it.name = "alsoResult"
}
println("alsoResult $alsoResult")

這里打印結(jié)果是alsoResult Book(name=alsoResult, price=20),看源碼的話,可以簡(jiǎn)單的里面為調(diào)用了一下傳入的函數(shù),然后返回了調(diào)用者

public inline fun <T> T.also(block: (T) -> Unit): T {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    block(this)
    return this
}

apply

函數(shù)聲明

public inline fun <T> T.apply(block: T.() -> Unit): T

可以看得出來(lái)apply是泛型 T 的擴(kuò)展函數(shù),接收一個(gè)帶有 T 類(lèi)型接收者的無(wú)參、無(wú)返回值的函數(shù),并且apply函數(shù)返回值就是 T 類(lèi)型,也就是調(diào)用者的類(lèi)型。因?yàn)檫@里參數(shù)中的 T 是作為接收者類(lèi)型,而不是參數(shù),所以在傳入的函數(shù)中需要用this而非it來(lái)指代調(diào)用者。 用法和also相差無(wú)幾,只不過(guò)一個(gè)是接收者類(lèi)型,一個(gè)是參數(shù)。

  • 上下文對(duì)象 作為接收者(this)來(lái)訪問(wèn)。
  • 返回值 是上下文對(duì)象本身。

對(duì)于不返回值且主要在接收者(this)對(duì)象的成員上運(yùn)行的代碼塊使用它。apply最常見(jiàn)的使用場(chǎng)景是用于對(duì)象配置。這樣的調(diào)用可以理解為將以下賦值操作應(yīng)用于對(duì)象

val applyResult = book.apply {
    changePrice(200)
    name = "applyResult"
}
println("applyResult $applyResult")

這里打印的結(jié)果是applyResult Book(name=applyResult, price=200). 源碼也和also幾乎一樣

public inline fun <T> T.also(block: (T) -> Unit): T {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    block(this)
    return this
}

let

函數(shù)類(lèi)型聲明如下:

public inline fun <T, R> T.let(block: (T) -> R): R

可以看到,let 是對(duì)泛型 T 的擴(kuò)展函數(shù),該擴(kuò)展函數(shù)接收一個(gè)函數(shù)參數(shù),并且函數(shù)參數(shù)的接收一個(gè) T 類(lèi)型的參數(shù),且返回值是 R 類(lèi)型,也是let這個(gè)擴(kuò)展函數(shù)的返回值類(lèi)型。

  • 上下文對(duì)象作為 lambda 表達(dá)式的參數(shù)(it)來(lái)訪問(wèn)。
  • 返回值是 lambda 表達(dá)式的結(jié)果。
val letResult = book.let {
    it.changePrice(100)
    it.name = "letResult"
}
println("letResult $letResult")

這里傳入的是一個(gè) Lambda 表達(dá)式,前面說(shuō)過(guò),對(duì)于單參數(shù)值的Lambda 表達(dá)式,參數(shù)會(huì)被隱式聲明為it,當(dāng)然我們也可以指定一個(gè)具名意義的變量,比如

val letResult = book.let { bookEntry: Book ->
    bookEntry.changePrice(100)
    bookEntry.name = "letResult"
}

這里打印的結(jié)果是letResult kotlin.Unit。因?yàn)閷?duì)于 Lambda 表達(dá)式來(lái)講,如果最后一條語(yǔ)句是非賦值語(yǔ)句,則返回該語(yǔ)句的值;如果是賦值語(yǔ)句,則返回 Unit。 我們可以這么寫(xiě)來(lái)返回我們需要的值:

val letResult = book.let {
    it//返回值就是傳入的 book 對(duì)象
}
val letResult = book.let {
    1//返回值就是1
}
val letResult = book.let {
     return@let 1//之前的文章中說(shuō)過(guò)的顯示指定返回值,是 1
}

從另外一個(gè)角度看,letalsoapply也差不多,只不過(guò)多了一個(gè)返回值類(lèi)型,返回值就是傳入的 Lambda 表達(dá)式的返回值 源碼也差不了多少

public inline fun <T, R> T.let(block: (T) -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block(this)
}

  • let 可用于在調(diào)用鏈的結(jié)果上調(diào)用一個(gè)或多個(gè)函數(shù)。
  • let 經(jīng)常用于執(zhí)行包含非空值代碼塊。如需對(duì)非空對(duì)象執(zhí)行操作, 可對(duì)其使用安全調(diào)用操作符?.并調(diào)用 let 在 lambda 表達(dá)式中執(zhí)行操作。

run

run這個(gè)函數(shù)給了兩種方式

public inline fun <T, R> T.run(block: T.() -> R): R
public inline fun <R> run(block: () -> R): R

先看第一種,看起來(lái)就是把let中函數(shù)參數(shù)中的 T 類(lèi)型參數(shù)改成了接收者類(lèi)型,也是返回 R 類(lèi)型;這和applyalso的區(qū)別是一樣的。

  • 上下文對(duì)象 作為接收者(this)來(lái)訪問(wèn)。
  • 返回值 是 lambda 表達(dá)式結(jié)果。

當(dāng) lambda 表達(dá)式同時(shí)初始化對(duì)象并計(jì)算返回值時(shí),run 很有用。

val runResult = book.run {
    name = "runResult"
    changePrice(110)
    this //作為返回值
}
println("runResult $runResult")

源碼是這樣的

public inline fun <T, R> T.run(block: T.() -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block()
}

第二種

val otherRunResult =  run {
    Book("run", 120) //作為返回值
}
println("otherRunResult $otherRunResult")

源碼

public inline fun <R> run(block: () -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block()
}

這也沒(méi)啥好說(shuō)的,只不過(guò)是這里并沒(méi)有輸入?yún)?shù),只是可以使你在需要表達(dá)式的地方就可以執(zhí)行一個(gè)語(yǔ)句。

with

函數(shù)聲明

public inline fun <T, R> with(receiver: T, block: T.() -> R): R

with并不是擴(kuò)展函數(shù),需要傳入一個(gè)T 類(lèi)型的receiver,可以在 block 中訪問(wèn)這個(gè)receiver的方法和屬性,

  • 上下文對(duì)象作為接收者(this)使用。
  • 返回值是 lambda 表達(dá)式結(jié)果。

建議當(dāng)不需要使用 lambda 表達(dá)式結(jié)果時(shí),使用 with 來(lái)調(diào)用上下文對(duì)象上的函數(shù)。 在代碼中,with 可以理解為對(duì)于這個(gè)對(duì)象,執(zhí)行以下操作.

val withResult = with(book) {
    changePrice(300)
    name = "withResult"
    this //作為返回值
}
println("withResult $withResult")

這里的打印結(jié)果是withResult Book(name=withResult, price=300)

如何選擇

這里再搬運(yùn)一個(gè)總結(jié)的表格

函數(shù)名 作用 應(yīng)用場(chǎng)景 備注
let 定義一個(gè)變量在特定作用域內(nèi)
統(tǒng)一做判空處理 明確一個(gè)變量所處特定的作用域范圍內(nèi)可使用
針對(duì)一個(gè)可空對(duì)象統(tǒng)一做判空處理 區(qū)別在于返回值
let函數(shù):返回值=最后一行 return的表達(dá)式
also函數(shù):返回值=傳入對(duì)象本身
also
with 調(diào)用同一個(gè)對(duì)象的多個(gè)方法 屬性時(shí),可以省去對(duì)象名,直接調(diào)用方法、訪問(wèn)屬性 需要多次調(diào)用同一個(gè)對(duì)象的屬性 方法 返回值=最后一行 return表達(dá)式
run 結(jié)合了let 函數(shù)和 with 函數(shù)的作用 1.調(diào)用同一個(gè)對(duì)象的多個(gè)方法/屬性時(shí)可以省去對(duì)象名重復(fù),直接調(diào)用方法名 /屬性即可

2.定義一個(gè)變量在特定作用域內(nèi)
3.統(tǒng)一做判空處 | 優(yōu)點(diǎn):避免了let函數(shù)必須使用it參數(shù)替代對(duì)象彌補(bǔ)了with函數(shù)無(wú)法判空的缺點(diǎn) |
| apply | 對(duì)象實(shí)例初始化時(shí)需要對(duì)對(duì)象中的屬性進(jìn)行賦值且返回該對(duì)象 | 二者區(qū)別在于返回值:
run函數(shù)返回最后一行的值|表達(dá)式
apply函數(shù)返回傳入的對(duì)象的本身 |

另外一個(gè)角度的選擇

it or this

每個(gè)作用域函數(shù)都使用以下兩種方式之一來(lái)引用上下文對(duì)象

  1. 作為 lambda 表達(dá)式的接收者 (this)
  2. 作為 lambda 表達(dá)式的參數(shù)(it)

兩者都提供了同樣的功能,runwith以及apply通過(guò)關(guān)鍵字this將上下文對(duì)象引用為lambda表達(dá)式的接收者。 因此,在它們的lambda表達(dá)式中可以像在普通的類(lèi)函數(shù)中一樣訪問(wèn)上下文對(duì)象。在大多數(shù)場(chǎng)景,當(dāng)你訪問(wèn)接收者對(duì)象時(shí)你可以省略this, 來(lái)讓你的代碼更簡(jiǎn)短。 相對(duì)地,如果省略了this,就很難區(qū)分接收者對(duì)象的成員及外部對(duì)象或函數(shù)。因此,對(duì)于主要對(duì)對(duì)象的成員進(jìn)行操作(調(diào)用其函數(shù)或賦值其屬性)的lambda表達(dá)式, 建議將上下文對(duì)象作為接收者(this)。 反過(guò)來(lái),letalso將上下文對(duì)象引用為lambda表達(dá)式參數(shù)。如果沒(méi)有指定參數(shù)名,對(duì)象可以用隱式默認(rèn)名稱(chēng)it訪問(wèn)。itthis簡(jiǎn)短,帶有it的表達(dá)式通常更易讀。不過(guò),當(dāng)調(diào)用對(duì)象函數(shù)或?qū)傩詴r(shí),不能像this這樣隱式地訪問(wèn)對(duì)象。 因此,當(dāng)上下文對(duì)象在作用域中主要用作函數(shù)調(diào)用中的參數(shù)時(shí),通過(guò)it訪問(wèn)上下文對(duì)象會(huì)更好。 在代碼塊中使用多個(gè)變量時(shí),it也更好一些。

返回值

根據(jù)返回結(jié)果,作用域函數(shù)可以分為以下兩類(lèi):

apply 及 also 返回上下文對(duì)象。 let、run 及 with 返回 lambda 表達(dá)式結(jié)果. apply 及 also 的返回值是上下文對(duì)象本身。因此,它們可以作為輔助步驟包含在調(diào)用鏈中:可以繼續(xù)在同一個(gè)對(duì)象上一個(gè)接一個(gè)地進(jìn)行鏈?zhǔn)胶瘮?shù)調(diào)用。

寫(xiě)在最后的注意事項(xiàng)

在最開(kāi)始的紅色部分也提高過(guò)盡量不要嵌套使用作用域函數(shù),警惕引發(fā)的上下文混淆。看下面的代碼猜一下打印結(jié)果是什么。

fun main() {
    val length = 0
    "hello".apply {
        println("this is apply $length")
        println("this is apply ${this.length}")
    }

    "hello".let {
        println("this is let $it")
        "world".also {
            println("this is run $it")
        }
    }

    fun innerFunc(){
        "hi".apply {
            println("this is innerFunc apply $length")
            println("this is innerFunc apply ${this.length}")

        }
    }
    innerFunc()
}

結(jié)果是如下:

this is apply 0 this is apply 5 this is let hello this is run world this is innerFunc apply 0 this is innerFunc apply 2

這里我們?cè)趯?xiě)代碼的時(shí)候,IDE 給了提示:Implicit parameter 'it' of enclosing lambda is shadowed

我們可以通過(guò)修改隱式 it 的名字來(lái)避免這個(gè)問(wèn)題

"hello".let {
    println("this is let $it")
    "world".also { world->
        println("this is run $world")
    }
}

但最好還是避免這種嵌套調(diào)用的情況。


?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容