剛開(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ù)。 共有以下五種:let
、run
、with
、apply
以及also
。 廢話不多說(shuō),先把從 kotlin 官方上扒拉下來(lái)的結(jié)論放這里
總結(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ì)象與this
或it
的值。
使用示例
假如我們有這么一個(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è)角度看,let
和 also
、apply
也差不多,只不過(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)型;這和apply
與also
的區(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ì)象
- 作為 lambda 表達(dá)式的接收者 (this)
- 作為 lambda 表達(dá)式的參數(shù)(it)
兩者都提供了同樣的功能,run
、with
以及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),let
及also
將上下文對(duì)象引用為lambda表達(dá)式參數(shù)
。如果沒(méi)有指定參數(shù)名,對(duì)象可以用隱式默認(rèn)名稱(chēng)it
訪問(wèn)。it
比this
簡(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)用的情況。