總結(jié)
let、with、run、apply、also 這幾個(gè)的作用就是在一個(gè)對(duì)象的上下文中執(zhí)行一段代碼。當(dāng)我們使用 lambda 表達(dá)式在一個(gè)對(duì)象上調(diào)用這樣的一個(gè)函數(shù)時(shí),它就形成了一個(gè)暫時(shí)的域,在這個(gè)域中,可以在不使用變量名的情況下獲取到對(duì)象,因此,這些函數(shù)也被稱為作用域函數(shù)(Scope functions)
run、with 和 apply 的上下文對(duì)象是 this ,可以省略掉 this 單詞,因此主要用于操作對(duì)象成員(例如調(diào)用對(duì)象的方法或使用其屬性)的時(shí)候
let 和 also 的上下文對(duì)象是 it , 適用于將此對(duì)象作為方法調(diào)用參數(shù)傳入
apply 和 also 返回上下文對(duì)象,可用于鏈?zhǔn)秸{(diào)用和返回上下文對(duì)象的函數(shù)的返回語(yǔ)句
let、run 和 with 返回 lambda 結(jié)果,可以在將結(jié)果分配給變量時(shí)使用
| 方法 | 上下文對(duì)象 | 返回值 | 擴(kuò)展函數(shù) | 使用場(chǎng)景 |
|---|---|---|---|---|
| let | it | lambda 結(jié)果 | 是 | 在非空對(duì)象上執(zhí)行一個(gè) lambda或者在局部域中引入一個(gè)變量 |
| with | this | lambda 結(jié)果 | 否 | 在一個(gè)對(duì)象上組合函數(shù)調(diào)用 |
| run | this | lambda 結(jié)果 | 是 | 對(duì)象配置并計(jì)算結(jié)果 |
| apply | this | 上下文對(duì)象 | 是 | 對(duì)象配置 |
| also | it | 上下文對(duì)象 | 是 | 額外的效果 |
| 非擴(kuò)展函數(shù) run | 無(wú) | lambda 結(jié)果 | 否 | 在需要表達(dá)式的地方運(yùn)行語(yǔ)句 |
表格里的上下文對(duì)象中,this 是可以省略的, it 是隱含的默認(rèn)參數(shù)名,可以顯式地指定為其他名字
let
@kotlin.internal.InlineOnly
public inline fun <T, R> T.let(block: (T) -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return block(this)
}
可以看到第一句代碼是 Contract ,這就需要引出 Contract 的內(nèi)容
Contract 示例
fun String?.isNull(): Boolean {
return this == null
}
fun test() {
var str: String? = null
if (str.isNull()) {
str = "kotlin contract"
}
println(str.length)
}
如上的代碼中,先為 String? 定義一個(gè)擴(kuò)展函數(shù) isNull(),用于判斷是否為 null,在 test 函數(shù)中,聲明一個(gè) String? 類型的 str ,使用 isNull() 判斷,如果其為 null ,就為其賦值,這樣,在下面調(diào)用 str.length 的時(shí)候 str 就一定不會(huì)為 null ,但實(shí)際上,這里還是會(huì)編譯報(bào)錯(cuò):
Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type String?
這是因?yàn)榫幾g器無(wú)法分析每個(gè)函數(shù),不能得到 str 不為 null 的結(jié)果,也就無(wú)法將 String? 智能轉(zhuǎn)換為 String ,Contract 就可以解決問(wèn)題,它可以向編譯器通知函數(shù)行為,上面的代碼修改為如下,就不會(huì)報(bào)錯(cuò)了
@ExperimentalContracts
fun String?.isNull(): Boolean {
//下面是添加的內(nèi)容
contract {
returns(false) implies (this@isNull != null)
}
//上面是添加的內(nèi)容
return this == null
}
@ExperimentalContracts
fun test() {
var str: String? = null
if (str.isNull()) {
str = "kotlin contract"
}
println(str.length)
}
上面代碼中添加內(nèi)容是告訴編譯器:如果返回值為 false ,那么 this(函數(shù)的接收者)不為 null
Contract 的概念
Contract 是一種向編譯器通知函數(shù)行為的方法,有以下特點(diǎn):
- 只能在 top-level 函數(shù)體內(nèi)使用 Contract
- Contract 所調(diào)用的聲明必須是函數(shù)體內(nèi)第一條語(yǔ)句
- Kotlin 編譯器并不會(huì)驗(yàn)證 Contract,因此必須編寫正確合理的 Contract
- 內(nèi)聯(lián)化的函數(shù)(也需要是 top-level 層級(jí)的函數(shù))支持使用 Contract
Contract 的分類
Returns Contracts
表示當(dāng) return 的返回值是某個(gè)值(例如true、false、null)時(shí),implies 后面的條件成立,有以下幾種形式:
| 形式 | 說(shuō)明 |
|---|---|
| returns(value: Any?) implies 條件 | 如果函數(shù)返回值為 value,條件成立 |
| returns() implies 條件 | 如果函數(shù)能夠正常返回,且沒(méi)有拋出異常,條件成立 |
| returnsNotNull implies 條件 | 如果函數(shù)返回非 null 值,條件成立 |
CallsInPlace Contracts
CallsInPlace Contracts 允許開(kāi)發(fā)者對(duì)調(diào)用的 lambda 表達(dá)式進(jìn)行頻率上的約束,只能在 inline 函數(shù)中調(diào)用
前面的高階函數(shù) let 就是一個(gè) CallsInPlace Contracts
@kotlin.internal.InlineOnly
public inline fun <T, R> T.let(block: (T) -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return block(this)
}
contract() 中的 callsInPlace 會(huì)告訴編譯器,lambda 表達(dá)式 block 在 let 函數(shù)內(nèi)只會(huì)執(zhí)行一次
callsInPlace() 中的 InvocationKind 是一個(gè)枚舉類,包含如下的枚舉值
| 枚舉值 | 說(shuō)明 |
|---|---|
| AT_MOST_ONCE | 函數(shù)參數(shù)調(diào)用次數(shù) <= 1 |
| EXACTLY_ONCE | 函數(shù)參數(shù)調(diào)用次數(shù) == 1 |
| AT_LEAST_ONCE | 函數(shù)參數(shù)調(diào)用次數(shù) >= 1 |
| UNKNOWN | 函數(shù)參數(shù)調(diào)用次數(shù) 不限制 |
以上 contract 內(nèi)容就結(jié)束了,下面看 let 函數(shù)的實(shí)現(xiàn),首先要明白兩點(diǎn):
- 在擴(kuò)展函數(shù)內(nèi)部,你可以像成員函數(shù)那樣使用 this 來(lái)引用接收者對(duì)象
- 當(dāng) lambda 表達(dá)式只有一個(gè)參數(shù),可以用 it 關(guān)鍵字來(lái)引用唯一的實(shí)參
源碼
上面的源代碼里可以看到:
let 函數(shù)是類型 T 的擴(kuò)展函數(shù),返回類型為 R,只有一個(gè)參數(shù),即 block,類型為 (T) -> R,指代 參數(shù)為 T ,返回值為 R 的函數(shù),因此上下文對(duì)象為 it ,指代 block 的唯一參數(shù) T 。返回值為 block(this) ,是調(diào)用 block 指代的函數(shù),并返回 block 函數(shù)的返回值,也就是返回 lambda 的結(jié)果
示例
在非空對(duì)象上執(zhí)行一個(gè) lambda
val str: String? = "Hello"
//processNonNullString(str) // compilation error: str can be null
val length = str?.let {
println("let() called on $it")
processNonNullString(it) // OK: 'it' is not null inside '?.let { }'
it.length
}
為了提高可讀性,在局部域中引入一個(gè)變量
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 + "!"
}.uppercase()
println("First item after modifications: '$modifiedFirstItem'")
with
源碼
@kotlin.internal.InlineOnly
public inline fun <T, R> with(receiver: T, block: T.() -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return receiver.block()
}
with 函數(shù)不是擴(kuò)展函數(shù),而是將類型 T 的一個(gè)對(duì)象 receiver 作為參數(shù)傳入 with 函數(shù),同時(shí)傳入 block 參數(shù),block 的類型為 T.() -> R ,即 類型 T 的一個(gè)無(wú)參且返回值為類型 R 的擴(kuò)展函數(shù),而 block 函數(shù)的調(diào)用者就是前面?zhèn)魅氲?receiver 參數(shù),因此上下文對(duì)象為 this ,指代擴(kuò)展函數(shù)的接收者 receiver ,返回值為 receiver.block() ,是調(diào)用 block 指代的函數(shù),并返回 block 函數(shù)的返回值,也就是返回 lambda 的結(jié)果
示例
推薦使用情況:調(diào)用對(duì)象的方法和屬性,但不返回結(jié)果。意味著 “with this object, do the following”
val numbers = mutableListOf("one", "two", "three")
with(numbers) {
println("'with' is called with argument $this")
println("It contains $size elements")
}
使用對(duì)象的屬性或方法計(jì)算出一個(gè)結(jié)果
val numbers = mutableListOf("one", "two", "three")
val firstAndLast = with(numbers) {
"The first element is ${first()}," +
" the last element is ${last()}"
}
println(firstAndLast)
run
run 有兩個(gè),一個(gè)是擴(kuò)展函數(shù) run ,一個(gè)是非擴(kuò)展函數(shù) run
源碼
擴(kuò)展函數(shù) run
@kotlin.internal.InlineOnly
public inline fun <T, R> T.run(block: T.() -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return block()
}
run 函數(shù)是類型 T 的一個(gè)擴(kuò)展函數(shù),返回值類型為 R,只有一個(gè)參數(shù),即 block,類型為 T.() -> R ,即 T 的一個(gè)無(wú)參且返回值類型為 R 的擴(kuò)展函數(shù),因此上下文對(duì)象為 this ,指代調(diào)用 block 擴(kuò)展函數(shù)的接收者,也就是調(diào)用 run 函數(shù)的對(duì)象,返回值為 block() ,是調(diào)用 block 指代的函數(shù),并返回 block 函數(shù)的返回值,也就是返回 lambda 的結(jié)果
非擴(kuò)展函數(shù) run
@kotlin.internal.InlineOnly
public inline fun <R> run(block: () -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return block()
}
run 函數(shù)不是擴(kuò)展函數(shù),返回類型為 R , run 函數(shù)的唯一參數(shù) block 的類型為 () -> R ,即 一個(gè)無(wú)參且返回類型為 R 的函數(shù)類型,因此 沒(méi)有上下文對(duì)象,返回值為 block() ,是調(diào)用 block 指代的函數(shù),并返回 block 函數(shù)的返回值,也就是返回 lambda 的結(jié)果
示例
擴(kuò)展函數(shù) run
同時(shí)包含對(duì)象初始化和返回值的計(jì)算
val service = MultiportService("https://example.kotlinlang.org", 80)
val result = service.run {
port = 8080
query(prepareRequest() + " to port $port")
}
非擴(kuò)展函數(shù) run
需要返回值的情況下執(zhí)行由多個(gè)語(yǔ)句組成的塊
val hexNumberRegex = run {
val digits = "0-9"
val hexDigits = "A-Fa-f"
val sign = "+-"
Regex("[$sign]?[$digits$hexDigits]+")
}
for (match in hexNumberRegex.findAll("+123 -FFFF !%*& 88 XYZ")) {
println(match.value)
}
apply
源碼
@kotlin.internal.InlineOnly
public inline fun <T> T.apply(block: T.() -> Unit): T {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
block()
return this
}
apply 函數(shù)是類型 T 的一個(gè)擴(kuò)展函數(shù),且其返回值類型為 T 類型,唯一的參數(shù) block 的類型為 T.() -> Unit,即 T 的一個(gè)無(wú)參無(wú)返回值擴(kuò)展函數(shù),因此上下文對(duì)象為 this ,指代調(diào)用 block 擴(kuò)展函數(shù)的接收者,也就是調(diào)用 apply 函數(shù)的對(duì)象 ,返回值為 this,也就是調(diào)用者,即返回上下文對(duì)象
示例
對(duì)象配置。意味著 “apply the following assignments to the object”
val adam = Person("Adam").apply {
age = 32
city = "London"
}
println(adam)
also
源碼
public inline fun <T> T.also(block: (T) -> Unit): T {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
block(this)
return this
}
also 函數(shù)是類型 T 的一個(gè)擴(kuò)展函數(shù),且其返回值類型為 T 類型,block 參數(shù)的類型為 (T) -> Unit,即 參數(shù)為 T 且無(wú)返回值的函數(shù),因此上下文對(duì)象為 it ,指代 block 的唯一參數(shù) T ,返回值為 this,也就是調(diào)用者,即返回上下文對(duì)象
示例
將上下文作為參數(shù)。意味著 "and also do the following with the object"
val numbers = mutableListOf("one", "two", "three")
numbers
.also { println("The list elements before adding new one: $it") }
.add("four")