
什么是作用域函數(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、apply、also。接下來我們逐個(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、with 和 apply 通過 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
let、also 有一個(gè)作為 lambda 參數(shù)傳入的 context 對(duì)象,如果不指定參數(shù)名,則可以通過該 context 對(duì)象的隱式默認(rèn)名稱 it 來訪問它,it 比 this 看上去更簡潔,用于表達(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ù)的返回值不同:
-
applay和also返回 context 對(duì)象 -
let、run、with返回閉包的運(yùn)算結(jié)果
返回 Context 對(duì)象
applay 和 also 返回 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é)果
let、run、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é)果
run 和 with 的作用類似,但是調(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ù)適用場景圖
