kotlin更多語言結構——>作用域函數
作用域函數
Kotlin 標準庫包含幾個函數,它們的唯一目的是在對象的上下文中執行代碼塊。當對一個對象調用這樣的函數 并提供一個 lambda 表達式時,它會形成一個臨時作用域。在此作用域中,可以訪問該對象而無需其名稱。這些 函數稱為作用域函數。共有以下五種:let 、run 、with 、apply 以及 also
這些函數基本上做了同樣的事情:在一個對象上執行一個代碼塊。不同的是這個對象在塊中如何使用,以及整個 表達式的結果是什么
下面是作用域函數的典型用法
Person("Alice", 20, "Amsterdam").let {
println(it)
it.moveTo("London")
it.incrementAge()
println(it)
}
如果不使用 let 來寫這段代碼,就必須引入一個新變量,并在每次使用它時重復其名稱
val alice = Person("Alice", 20, "Amsterdam")
println(alice)
alice.moveTo("London")
alice.incrementAge()
println(alice)
作用域函數沒有引入任何新的技術,但是它們可以使你的代碼更加簡潔易讀。
由于作用域函數的相似性質,為你的案例選擇正確的函數可能有點棘手。選擇主要取決于你的意圖和項目中使 用的一致性。下面我們將詳細描述各種作用域函數及其約定用法之間的區別
區別
由于作用域函數本質上都非常相似,因此了解它們之間的區別很重要。每個作用域函數之間有兩個主要區別:
— 引用上下文對象的方式
— 返回值
上下文對象:this 還是 it
在作用域函數的 lambda 表達式里,上下文對象可以不使用其實際名稱而是使用一個更簡短的引用來訪問。每 個作用域函數都使用以下兩種方式之一來訪問上下文對象:作為 lambda 表達式的接收者( this )或者作為 lambda 表達式的參數( it )。兩者都提供了同樣的功能,因此我們將針對不同的場景描述兩者的優缺點,并提供使用建議
fun main() {
val str = "Hello" // this
str.run {
println("The receiver string length: $length")
//println("The receiver string length: ${this.length}") // 和上句效果相同
}
// it
str.let {
println("The receiver string's length is ${it.length}")
}
}
run 、with 以及 apply 通過關鍵字 this 引用上下文對象。因此,在它們的 lambda 表達式中可以像在普 通的類函數中一樣訪問上下文對象。在大多數場景,當你訪問接收者對象時你可以省略 this,來讓你的代碼更 簡短。相對地,如果省略了 this,就很難區分接收者對象的成員及外部對象或函數。因此,對于主要對對象成員 進行操作(調用其函數或賦值其屬性)的 lambda 表達式,建議將上下文對象作為接收者( this )
val adam = Person("Adam").apply {
age = 20 // 和 this.age = 20 或者 adam.age = 20 一樣
city = "London"
}
println(adam)
反過來,let 及 also 將上下文對象作為 lambda 表達式參數。如果沒有指定參數名,對象可以用隱式默認名 稱 it 訪問。it 比 this 簡短,帶有 it 的表達式通常更容易閱讀。然而,當調用對象函數或屬性時,不能像this 這樣隱式地訪問對象。因此,當上下文對象在作用域中主要用作函數調用中的參數時,使用 it 作為上下 文對象會更好。若在代碼塊中使用多個變量,則 it 也更好
fun getRandomInt(): Int {
return Random.nextInt(100).also {
writeToLog("getRandomInt() generated value $it")
}
}
val i = getRandomInt()
此外,當將上下文對象作為參數傳遞時,可以為上下文對象指定在作用域內的自定義名稱
fun getRandomInt(): Int {
return Random.nextInt(100).also { value ->
writeToLog("getRandomInt() generated value $value")
}
}
val i = getRandomInt()
返回值
根據返回結果,作用域函數可以分為以下兩類
— apply 及 also 返回上下文對象。
— let 、run 及 with 返回 lambda 表達式結果.
這兩個選項使你可以根據在代碼中的后續操作來選擇適當的函數。
apply 及 also 的返回值是上下文對象本身。因此,它們可以作為輔助步驟包含在調用鏈中:你可以繼續在同 一個對象上進行鏈式函數調用
val numberList = mutableListOf<Double>()
numberList.also { println("Populating the list") }
.apply {
add(2.71)
add(3.14)
add(1.0) }
.also {
println("Sorting the list")
}
.sort()
它們還可以用在返回上下文對象的函數的 return 語句中
fun getRandomInt(): Int {
return Random.nextInt(100).also {
writeToLog("getRandomInt() generated value $it")
}
}
val i = getRandomInt()
let 、run 及 with 返回 lambda 表達式的結果。所以,在需要使用其結果給一個變量賦值,或者在需要對其結果進行鏈式操作等情況下,可以使用它們
val numbers = mutableListOf("one", "two", "three")
val countEndsWithE = numbers.run {
add("four")
add("five")
count { it.endsWith("e") }
}
println("There are $countEndsWithE elements that end with e.")
此外,還可以忽略返回值,僅使用作用域函數為變量創建一個臨時作用域。
val numbers = mutableListOf("one", "two", "three")
with(numbers) {
val firstItem = first()
val lastItem = last()
println("First item: $firstItem, last item: $lastItem")
}
幾個函數
為了幫助你為你的場景選擇合適的作用域函數,我們會詳細地描述它們并且提供一些使用建議。從技術?度來 說,作用域函數在很多場景里是可以互換的,所以這些示例展示了定義通用使用?格的約定用法
let
上下文對象作為 lambda 表達式的參數( it )來訪問。返回值是 lambda 表達式的結果。
let 可用于在調用鏈的結果上調用一個或多個函數。例如,以下代碼打印對集合的兩個操作的結果
val numbers = mutableListOf("one", "two", "three", "four", "five")
val resultList = numbers.map { it.length }.filter { it > 3 }
println(resultList)
使用 let,可以寫成這樣
val numbers = mutableListOf("one", "two", "three", "four", "five")
numbers.map { it.length }.filter { it > 3 }.let {
println(it)
// 如果需要可以調用更多函數
}
若代碼塊僅包含以 it 作為參數的單個函數,則可以使用方法引用( :: )代替 lambda 表達式
val numbers = mutableListOf("one", "two", "three", "four", "five")
numbers.map { it.length }.filter { it > 3 }.let(::println)
let 經常用于僅使用非空值執行代碼塊。如需對非空對象執行操作,可對其使用安全調用操作符 ?. 并調用 let 在 lambda 表達式中執行操作
val str: String? = "Hello"
//processNonNullString(str) // 編譯錯誤:str 可能為空
val length = str?.let {
println("let() called on $it")
processNonNullString(it) // 編譯通過:'it' 在 '?.let { }' 中必不為空
it.length
}
使用 let 的另一種情況是引入作用域受限的局部變量以提高代碼的可讀性。如需為上下文對象定義一個新變 量,可提供其名稱作為 lambda 表達式參數來替默認的 it
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
一個非擴展函數:上下文對象作為參數傳遞,但是在 lambda 表達式內部,它可以作為接收者( this )使用。返 回值是 lambda 表達式結果
我們建議使用 with 來調用上下文對象上的函數,而不使用 lambda 表達式結果。在代碼中,with 可以理解 為“對于這個對象,執行以下操作
val numbers = mutableListOf("one", "two", "three")
with(numbers) {
println("'with' is called with argument $this")
println("It contains $size elements")
}
with 的另一個使用場景是引入一個輔助對象,其屬性或函數將用于計算一個值
val numbers = mutableListOf("one", "two", "three")
val firstAndLast = with(numbers) {
"The first element is ${first()}," +
" the last element is ${last()}"
}
println(firstAndLast)
run
上下文對象 作為接收者( this )來訪問。返回值 是 lambda 表達式結果。
run 和 with 做同樣的事情,但是調用方式和 let 一樣——作為上下文對象的擴展函數
當 lambda 表達式同時包含對象初始化和返回值的計算時,run 很有用。
val service = MultiportService("https://example.kotlinlang.org", 80)
val result = service.run {
port = 8080
query(prepareRequest() + " to port $port")
}
// 同樣的代碼如果用 let() 函數來寫:
val letResult = service.let {
it.port = 8080
it.query(it.prepareRequest() + " to port ${it.port}")
}
除了在接收者對象上調用 run 之外,還可以將其用作非擴展函數。非擴展 run 可以使你在需要表達式的地方 執行一個由多個語句組成的塊
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
上下文對象 作為接收者( this )來訪問。返回值 是上下文對象本身。
對于不返回值且主要在接收者(this)對象的成員上運行的代碼塊使用 apply。apply 的常?情況是對象配置。這樣的調用可以理解為“將以下賦值操作應用于對象
val adam = Person("Adam").apply {
age = 32
city = "London"
}
println(adam)
將接收者作為返回值,你可以輕松地將 apply 包含到調用鏈中以進行更復雜的處理
also
上下文對象作為 lambda 表達式的參數( it )來訪問。返回值是上下文對象本身
also 對于執行一些將上下文對象作為參數的操作很有用。對于需要引用對象而不是其屬性與函數的操作,或者不想屏蔽來自外部作用域的 this 引用時,請使用 also
當你在代碼中看到 also 時,可以將其理解為“并且用該對象執行以下操作”。
val numbers = mutableListOf("one", "two", "three")
numbers
.also { println("The list elements before adding new one: $it") }
.add("four")
函數選擇
為了幫助你選擇合適的作用域函數,我們提供了它們之間的主要區別表
| 函數 | 對象引用 | 返回值 | 是否是擴展函數 |
| let | it | Lambda表達式結果 | 是 |
| run | this | Lambda表達式結果 | 是 |
| run | - | Lambda表達式結果 | 不是:調用無需上下文對象 |
| with | this | Lambda表達式結果 | 不是:把上下文對象當作參數 |
| apply | this | 上下文對象 | 是 |
| also | it | 上下文對象 | 是 |
以下是根據預期目的選擇作用域函數的簡短指南:
— 對一個非空(non-null)對象執行lambda表達式:let — 將表達式作為變量引入為局部作用域中:let
— 對象配置:apply
— 對象配置并且計算結果:run
— 在需要表達式的地方運行語句:非擴展的 run
— 附加效果:also
— 一個對象的一組函數調用:with
不同函數的使用場景存在重疊,你可以根據項目或團隊中使用的特定約定選擇函數
盡管作用域函數是使代碼更簡潔的一種方法,但請避免過度使用它們:這會降低代碼的可讀性并可能導致錯誤。 避免嵌套作用域函數,同時鏈式調用它們時要小心:此時很容易對當前上下文對象及 this 或 it 的值感到困 惑。
takeIf 與 takeUnless
除了作用域函數外,標準庫還包含函數 takeIf 及 takeUnless 。這倆函數使你可以將對象狀態檢查嵌入到調用鏈中。
當以提供的謂詞在對象上進行調用時,若該對象與謂詞匹配,則 takeIf 返回此對象。否則返回 null 。因 此,takeIf 是單個對象的過濾函數。反之,takeUnless 如果不匹配謂詞,則返回對象,如果匹配則返回null 。該對象作為 lambda 表達式參數( it )來訪問。
val number = Random.nextInt(100)
val evenOrNull = number.takeIf { it % 2 == 0 }
val oddOrNull = number.takeUnless { it % 2 == 0 }
println("even: $evenOrNull, odd: $oddOrNull")
當在 takeIf 及 takeUnless 之后鏈式調用其他函數,不要忘記執行空檢查或安全調用(?.),因為他們的 返回值是可為空的
val str = "Hello"
val caps = str.takeIf { it.isNotEmpty() }?.toUpperCase()
//val caps = str.takeIf { it.isNotEmpty() }.toUpperCase() // 編譯錯誤
println(caps)
takeIf 及 takeUnless 與作用域函數一起特別有用。一個很好的例子是用 let 鏈接它們,以便在與給定 謂詞匹配的對象上運行代碼塊。為此,請在對象上調用 takeIf,然后通過安全調用(?.)調用 let。對于與謂 詞不匹配的對象,takeIf 返回 null,并且不調用 let
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")
沒有標準庫函數時,相同的函數看起來是這樣的
fun displaySubstringPosition(input: String, sub: String) {
val index = input.indexOf(sub)
if (index >= 0) {
println("The substring $sub is found in $input.")
println("Its start position is $index.")
}
}
displaySubstringPosition("010000011", "11")
displaySubstringPosition("010000011", "12")

浙公網安備 33010602011771號