原文:Kotlin官方
Kotlin中的作用域函數(shù)共有以下五種:let
、run
、with
叉趣、apply
以及 also
唤崭。
這些函數(shù)并沒有做什么特殊的操作屋彪,只是將對當前對象的操作都集中到了一個代碼塊中來溃肪,寫出來和看起來更加干凈了而已均牢。
不同的是這個對象在塊中如何使用,以及整個表達式的結(jié)果是什么曹货。
區(qū)別
上下文對象(this/it) | 返回值 | |
---|---|---|
let | it | lambda 表達式結(jié)果 |
run | this(隱式訪問) | lambda 表達式結(jié)果 |
with | this(隱式訪問) | lambda 表達式結(jié)果 |
apply | this(隱式訪問) | 上下文 |
also | it | 上下文 |
let
上下文對象作為 lambda 表達式的參數(shù)(it
)來訪問咆繁。返回值是 lambda 表達式的結(jié)果。
Person("Alice", 20, "Amsterdam").let {
println(it)
it.moveTo("London")
it.incrementAge()
println(it)
}
let
經(jīng)常用于僅使用非空值執(zhí)行代碼塊控乾。如需對非空對象執(zhí)行操作么介,可對其使用安全調(diào)用操作符 ?.
并調(diào)用 let
在 lambda 表達式中執(zhí)行操作。
val str: String? = "Hello"
//processNonNullString(str) // 編譯錯誤:str 可能為空
val length = str?.let {
println("let() called on $it")
processNonNullString(it) // 編譯通過:'it' 在 '?.let { }' 中必不為空
it.length
}
使用 let
的另一種情況是引入作用域受限的局部變量以提高代碼的可讀性蜕衡。如需為上下文對象定義一個新變量壤短,可提供其名稱作為 lambda 表達式參數(shù)來替默認的 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
一個非擴展函數(shù):上下文對象作為參數(shù)傳遞慨仿,但是在 lambda 表達式內(nèi)部久脯,它可以作為接收者(this
)使用。 返回值是 lambda 表達式結(jié)果镰吆。
我們建議使用 with
來調(diào)用上下文對象上的函數(shù)帘撰,而不使用 lambda 表達式結(jié)果。 在代碼中万皿,with
可以理解為“對于這個對象摧找,執(zhí)行以下操作。”
val numbers = mutableListOf("one", "two", "three")
with(numbers) {
println("'with' is called with argument $this")
println("It contains $size elements")
}
with
的另一個使用場景是引入一個輔助對象牢硅,其屬性或函數(shù)將用于計算一個值蹬耘。
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 表達式結(jié)果减余。
run和with做一樣的事综苔,不過調(diào)用的方式和let一樣。
當 lambda 表達式同時包含對象初始化和返回值的計算時位岔,run
很有用如筛。
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}")
}
除了在接收者對象上調(diào)用 run
之外,還可以將其用作非擴展函數(shù)抒抬。 非擴展 run
可以使你在需要表達式的地方執(zhí)行一個由多個語句組成的塊杨刨。
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
)來訪問。 返回值 是上下文對象本身擦剑。
apply
的常見情況是對象配置拭嫁。這樣的調(diào)用可以理解為“將以下賦值操作應用于對象”。
val adam = Person("Adam").apply {
age = 32
city = "London"
}
println(adam)
also
上下文對象作為 lambda 表達式的參數(shù)(it
)來訪問抓于。 返回值是上下文對象本身做粤。
also
對于執(zhí)行一些將上下文對象作為參數(shù)的操作很有用。 對于需要引用對象而不是其屬性與函數(shù)的操作捉撮,或者不想屏蔽來自外部作用域的 this
引用時怕品,請使用 also
。
當你在代碼中看到 also
時巾遭,可以將其理解為“并且用該對象執(zhí)行以下操作”肉康。
val numbers = mutableListOf("one", "two", "three")
numbers
.also { println("The list elements before adding new one: $it") }
.add("four")
函數(shù)選擇
為了幫助你選擇合適的作用域函數(shù)闯估,我們提供了它們之間的主要區(qū)別表。
函數(shù) | 對象引用 | 返回值 | 是否是擴展函數(shù) |
---|---|---|---|
let |
it |
Lambda 表達式結(jié)果 | 是 |
run |
this |
Lambda 表達式結(jié)果 | 是 |
run |
- | Lambda 表達式結(jié)果 | 不是:調(diào)用無需上下文對象 |
with |
this |
Lambda 表達式結(jié)果 | 不是:把上下文對象當做參數(shù) |
apply |
this |
上下文對象 | 是 |
also |
it |
上下文對象 | 是 |
以下是根據(jù)預期目的選擇作用域函數(shù)的簡短指南:
- 對一個非空(non-null)對象執(zhí)行 lambda 表達式:
let
- 將表達式作為變量引入為局部作用域中:
let
- 對象配置:
apply
- 對象配置并且計算結(jié)果:
run
- 在需要表達式的地方運行語句:非擴展的
run
- 附加效果:
also
- 一個對象的一組函數(shù)調(diào)用:
with
不同函數(shù)的使用場景存在重疊吼和,你可以根據(jù)項目或團隊中使用的特定約定選擇函數(shù)涨薪。
盡管作用域函數(shù)是使代碼更簡潔的一種方法,但請避免過度使用它們:這會降低代碼的可讀性并可能導致錯誤炫乓。避免嵌套作用域函數(shù)刚夺,同時鏈式調(diào)用它們時要小心:此時很容易對當前上下文對象及 this
或 it
的值感到困惑。
takeIf
與 takeUnless
除了作用域函數(shù)外末捣,標準庫還包含函數(shù) takeIf
及 takeUnless
侠姑。這倆函數(shù)使你可以將對象狀態(tài)檢查嵌入到調(diào)用鏈中。
當以提供的謂詞在對象上進行調(diào)用時箩做,若該對象與謂詞匹配莽红,則 takeIf
返回此對象。否則返回 null
邦邦。因此安吁,takeIf
是單個對象的過濾函數(shù)。反之燃辖,takeUnless
如果不匹配謂詞鬼店,則返回對象,如果匹配則返回 null
郭赐。該對象作為 lambda 表達式參數(shù)(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
之后鏈式調(diào)用其他函數(shù)确沸,不要忘記執(zhí)行空檢查或安全調(diào)用(?.
)捌锭,因為他們的返回值是可為空的。
val str = "Hello"
val caps = str.takeIf { it.isNotEmpty() }?.toUpperCase()
//val caps = str.takeIf { it.isNotEmpty() }.toUpperCase() // 編譯錯誤
println(caps)
takeIf
及 takeUnless
與作用域函數(shù)一起特別有用罗捎。 一個很好的例子是用 let
鏈接它們观谦,以便在與給定謂詞匹配的對象上運行代碼塊。 為此桨菜,請在對象上調(diào)用 takeIf
豁状,然后通過安全調(diào)用(?.
)調(diào)用 let
。對于與謂詞不匹配的對象倒得,takeIf
返回 null
泻红,并且不調(diào)用 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")
沒有標準庫函數(shù)時霞掺,相同的函數(shù)看起來是這樣的:
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")