# 作用域函數
Kotlin 標準庫包含幾個函數,它們的唯一目的是在對象的上下文中執行代碼塊。當對一個對象調用這樣的函數并提供一個 [lambda 表達式](http://www.kotlincn.net/docs/reference/lambdas.html)時,它會形成一個臨時作用域。在此作用域中,可以訪問該對象而無需其名稱。這些函數稱為*作用域函數*。共有以下五種:`let`、`run`、`with`、`apply` 以及 `also`。
這些函數基本上做了同樣的事情:在一個對象上執行一個代碼塊。不同的是這個對象在塊中如何使用,以及整個表達式的結果是什么。
下面是作用域函數的典型用法:
```kotlin
data class Person(var name: String, var age: Int, var city: String) {
fun moveTo(newCity: String) { city = newCity }
fun incrementAge() { age++ }
}
fun main() {
Person("Alice", 20, "Amsterdam").let {
println(it)
it.moveTo("London")
it.incrementAge()
println(it)
}
}
```
如果不使用 `let` 來寫這段代碼, 就必須引入一個新變量,并在每次使用它時重復其名稱。
```kotlin
data class Person(var name: String, var age: Int, var city: String) {
fun moveTo(newCity: String) { city = newCity }
fun incrementAge() { age++ }
}
fun main() {
val alice = Person("Alice", 20, "Amsterdam")
println(alice)
alice.moveTo("London")
alice.incrementAge()
println(alice)
}
```
作用域函數沒有引入任何新的技術,但是它們可以使你的代碼更加簡潔易讀。
由于作用域函數的相似性質,為你的案例選擇正確的函數可能有點棘手。選擇主要取決于你的意圖和項目中使用的一致性。下面我們將詳細描述各種作用域函數及其約定用法之間的區別。
## 區別
由于作用域函數本質上都非常相似,因此了解它們之間的區別很重要。每個作用域函數之間有兩個主要區別:
* 引用上下文對象的方式
* 返回值
### 上下文對象:`this` 還是 `it`
在作用域函數的 lambda 表達式里,上下文對象可以不使用其實際名稱而是使用一個更簡短的引用來訪問。每個作用域函數都使用以下兩種方式之一來訪問上下文對象:作為 lambda 的[接收者](lambdas.html#帶有接收者的函數字面值)(`this`)或者作為 lambda 的參數(`it`)。兩者都提供了同樣的功能,因此我們將針對不同的場景描述兩者的優缺點,并提供使用建議。
```kotlin
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}")
}
}
```
#### this
`run`、`with` 以及 `apply` 通過關鍵字 `this` 引用上下文對象。因此,在它們的 lambda 表達式中可以像在普通的類函數中一樣訪問上下文對象。在大多數場景,當你訪問接收者對象時你可以省略 `this`,來讓你的代碼更簡短。相對地,如果省略了 `this`,就很難區分接收者對象的成員及外部對象或函數。因此,對于主要對對象成員進行操作(調用其函數或賦值其屬性)的 lambda,建議將上下文對象作為接收者(`this`)。
```kotlin
data class Person(var name: String, var age: Int = 0, var city: String = "")
fun main() {
val adam = Person("Adam").apply {
age = 20 // 和 this.age = 20 或者 adam.age = 20 一樣
city = "London"
}
}
```
#### it
反過來,`let` 及 `also` 將上下文對象作為 lambda 表達式參數。如果沒有指定參數名,對象可以用隱式默認名稱 `it` 訪問。`it` 比 `this` 簡短,帶有 `it` 的表達式通常更容易閱讀。然而,當調用對象函數或屬性時,不能像 `this` 這樣隱式地訪問對象。因此,當上下文對象在作用域中主要用作函數調用中的參數時,使用 `it` 作為上下文對象會更好。若在代碼塊中使用多個變量,則 `it` 也更好。
```kotlin
import kotlin.random.Random
fun writeToLog(message: String) {
println("INFO: $message")
}
fun main() {
fun getRandomInt(): Int {
return Random.nextInt(100).also {
writeToLog("getRandomInt() generated value $it")
}
}
val i = getRandomInt()
}
```
此外,當將上下文對象作為參數傳遞時,可以為上下文對象指定在作用域內的自定義名稱。
```kotlin
import kotlin.random.Random
fun writeToLog(message: String) {
println("INFO: $message")
}
fun main() {
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` 的返回值是上下文對象本身。因此,它們可以作為輔助步驟包含在調用鏈中:你可以繼續在同一個對象上進行鏈式函數調用。
```kotlin
fun main() {
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()
println(numberList)
}
```
它們還可以用在返回上下文對象的函數的 return 語句中。
```kotlin
import kotlin.random.Random
fun writeToLog(message: String) {
println("INFO: $message")
}
fun main() {
fun getRandomInt(): Int {
return Random.nextInt(100).also {
writeToLog("getRandomInt() generated value $it")
}
}
val i = getRandomInt()
}
```
#### Lambda 表達式結果
`let`、`run` 及 `with` 返回 lambda 表達式的結果。所以,在需要使用其結果給一個變量賦值,或者在需要對其結果進行鏈式操作等情況下,可以使用它們。
```kotlin
fun main() {
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.")
}
```
此外,還可以忽略返回值,僅使用作用域函數為變量創建一個臨時作用域。
```kotlin
fun main() {
val numbers = mutableListOf("one", "two", "three")
with(numbers) {
val firstItem = first()
val lastItem = last()
println("First item: $firstItem, last item: $lastItem")
}
}
```
## 幾個函數
To help you choose the right scope function for your case, we'll describe them in detail and provide usage recommendations. Technically, functions are interchangeable in many cases, so the examples show the conventions that define the common usage style.
### `let`
**The context object** is available as an argument (`it`). **The return value** is the lambda result.
`let` can be used to invoke one or more functions on results of call chains. For example, the following code prints the results of two operations on a collection:
```kotlin
fun main() {
val numbers = mutableListOf("one", "two", "three", "four", "five")
val resultList = numbers.map { it.length }.filter { it > 3 }
println(resultList)
}
```
With `let`, you can rewrite it:
```kotlin
fun main() {
val numbers = mutableListOf("one", "two", "three", "four", "five")
numbers.map { it.length }.filter { it > 3 }.let {
println(it)
// and more function calls if needed
}
}
```
If the code block contains a single function with `it` as an argument, you can use the method reference (`::`) instead of the lambda:
```kotlin
fun main() {
val numbers = mutableListOf("one", "two", "three", "four", "five")
numbers.map { it.length }.filter { it > 3 }.let(::println)
}
```
`let` is often used for executing a code block only with non-null values. To perform actions on a non-null object, use the safe call operator `?.` on it and call `let` with the actions in its lambda.
```kotlin
fun processNonNullString(str: String) {}
fun main() {
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
}
}
```
Another case for using `let` is introducing local variables with a limited scope for improving code readability. To define a new variable for the context object, provide its name as the lambda argument so that it can be used instead of the default `it`.
```kotlin
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`
A non-extension function: **the context object** is passed as an argument, but inside the lambda, it's available as a receiver (`this`). **The return value** is the lambda result.
We recommend `with` for calling functions on the context object without providing the lambda result. In the code, `with` can be read as “_with this object, do the following._”
```kotlin
fun main() {
val numbers = mutableListOf("one", "two", "three")
with(numbers) {
println("'with' is called with argument $this")
println("It contains $size elements")
}
}
```
Another use case for `with` is introducing a helper object whose properties or functions will be used for calculating a value.
```kotlin
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`
**The context object** is available as a receiver (`this`). **The return value** is the lambda result.
`run` does the same as `with` but invokes as `let` - as an extension function of the context object.
`run` is useful when your lambda contains both the object initialization and the computation of the return value.
```kotlin
class 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")
}
// the same code written with let() function:
val letResult = service.let {
it.port = 8080
it.query(it.prepareRequest() + " to port ${it.port}")
}
println(result)
println(letResult)
}
```
Besides calling `run` on a receiver object, you can use it as a non-extension function. Non-extension `run` lets you execute a block of several statements where an expression is required.
```kotlin
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`
**The context object** is available as a receiver (`this`). **The return value** is the object itself.
Use `apply` for code blocks that don't return a value and mainly operate on the members of the receiver object. The common case for `apply` is the object configuration. Such calls can be read as “_apply the following assignments to the object._”
```kotlin
data class Person(var name: String, var age: Int = 0, var city: String = "")
fun main() {
val adam = Person("Adam").apply {
age = 32
city = "London"
}
}
```
Having the receiver as the return value, you can easily include `apply` into call chains for more complex processing.
### `also`
**The context object** is available as an argument (`it`). **The return value** is the object itself.
`also` is good for performing some actions that take the context object as an argument. Use `also` for additional actions that don't alter the object, such as logging or printing debug information. Usually, you can remove the calls of `also` from the call chain without breaking the program logic.
When you see `also` in the code, you can read it as “_and also do the following_”.
```kotlin
fun main() {
val numbers = mutableListOf("one", "two", "three")
numbers
.also { println("The list elements before adding new one: $it") }
.add("four")
}
```
## 函數選擇
To help you choose the right scope function for your purpose, we provide the table of key differences between them.
|Function|Object reference|Return value|Is extension function|
|---|---|---|---|
|`let`|`it`|Lambda result|Yes|
|`run`|`this`|Lambda result|Yes|
|`run`|-|Lambda result|No: called without the context object|
|`with`|`this`|Lambda result|No: takes the context object as an argument.|
|`apply`|`this`|Context object|Yes|
|`also`|`it`|Context object|Yes|
Here is a short guide for choosing scope functions depending on the intended purpose:
* Executing a lambda on non-null objects: `let`
* Introducing an expression as a variable in local scope: `let`
* Object configuration: `apply`
* Object configuration and computing the result: `run`
* Running statements where an expression is required: non-extension `run`
* Additional effects: `also`
* Grouping function calls on an object: `with`
The use cases of different functions overlap, so that you can choose the functions based on the specific conventions used in your project or team.
Although the scope functions are a way of making the code more concise, avoid overusing them: it can decrease your code readability and lead to errors. Avoid nesting scope functions and be careful when chaining them: it's easy to get confused about the current context object and the value of `this` or `it`.
## `takeIf` 與 `takeUnless`
In addition to scope functions, the standard library contains the functions `takeIf` and `takeUnless`. These functions let you embed checks of the object state in call chains.
When called on an object with a predicate provided, `takeIf` returns this object if it matches the predicate. Otherwise, it returns `null`. So, `takeIf` is a filtering function for a single object. In turn, `takeUnless` returns the object if it doesn't match the predicate and `null` if it does. The object is available as a lambda argument (`it`).
```kotlin
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("even: $evenOrNull, odd: $oddOrNull")
}
```
When chaining other functions after `takeIf` and `takeUnless`, don't forget to perform the null check or the safe call (`?.`) because their return value is nullable.
```kotlin
fun main() {
val str = "Hello"
val caps = str.takeIf { it.isNotEmpty() }?.toUpperCase()
//val caps = str.takeIf { it.isNotEmpty() }.toUpperCase() //compilation error
println(caps)
}
```
`takeIf` and `takeUnless` are especially useful together with scope functions. A good case is chaining them with `let` for running a code block on objects that match the given predicate. To do this, call `takeIf` on the object and then call `let` with a safe call (`?`). For objects that don't match the predicate, `takeIf` returns `null` and `let` isn't invoked.
```kotlin
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")
}
```
This is how the same function looks without the standard library functions:
```kotlin
fun main() {
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")
}
```
- 前言
- Kotlin簡介
- IntelliJ IDEA技巧總結
- idea設置類注釋和方法注釋模板
- 像Android Studion一樣創建工程
- Gradle
- Gradle入門
- Gradle進階
- 使用Gradle創建一個Kotlin工程
- 環境搭建
- Androidstudio平臺搭建
- Eclipse的Kotlin環境配置
- 使用IntelliJ IDEA
- Kotlin學習路線
- Kotlin官方中文版文檔教程
- 概述
- kotlin用于服務器端開發
- kotlin用于Android開發
- kotlin用于JavaScript開發
- kotlin用于原生開發
- Kotlin 用于數據科學
- 協程
- 多平臺
- 新特性
- 1.1的新特性
- 1.2的新特性
- 1.3的新特性
- 開始
- 基本語法
- 習慣用法
- 編碼規范
- 基礎
- 基本類型
- 包與導入
- 控制流
- 返回與跳轉
- 類與對象
- 類與繼承
- 屬性與字段
- 接口
- 可見性修飾符
- 擴展
- 數據類
- 密封類
- 泛型
- 嵌套類
- 枚舉類
- 對象
- 類型別名
- 內嵌類
- 委托
- 委托屬性
- 函數與Lambda表達式
- 函數
- Lambda表達式
- 內聯函數
- 集合
- 集合概述
- 構造集合
- 迭代器
- 區間與數列
- 序列
- 操作概述
- 轉換
- 過濾
- 加減操作符
- 分組
- 取集合的一部分
- 取單個元素
- 排序
- 聚合操作
- 集合寫操作
- List相關操作
- Set相關操作
- Map相關操作
- 多平臺程序設計
- 平臺相關聲明
- 以Gradle創建
- 更多語言結構
- 解構聲明
- 類型檢測與轉換
- This表達式
- 相等性
- 操作符重載
- 空安全
- 異常
- 注解
- 反射
- 作用域函數
- 類型安全的構造器
- Opt-in Requirements
- 核心庫
- 標準庫
- kotlin.test
- 參考
- 關鍵字與操作符
- 語法
- 編碼風格約定
- Java互操作
- Kotlin中調用Java
- Java中調用Kotlin
- JavaScript
- 動態類型
- kotlin中調用JavaScript
- JavaScript中調用kotlin
- JavaScript模塊
- JavaScript反射
- JavaScript DCE
- 原生
- 并發
- 不可變性
- kotlin庫
- 平臺庫
- 與C語言互操作
- 與Object-C及Swift互操作
- CocoaPods集成
- Gradle插件
- 調試
- FAQ
- 協程
- 協程指南
- 基礎
- 取消與超時
- 組合掛起函數
- 協程上下文與調度器
- 異步流
- 通道
- 異常處理與監督
- 共享的可變狀態與并發
- Select表達式(實驗性)
- 工具
- 編寫kotlin代碼文檔
- 使用Kapt
- 使用Gradle
- 使用Maven
- 使用Ant
- Kotlin與OSGI
- 編譯器插件
- 編碼規范
- 演進
- kotlin語言演進
- 不同組件的穩定性
- kotlin1.3的兼容性指南
- 常見問題
- FAQ
- 與Java比較
- 與Scala比較(官方已刪除)
- Google開發者官網簡介
- Kotlin and Android
- Get Started with Kotlin on Android
- Kotlin on Android FAQ
- Android KTX
- Resources to Learn Kotlin
- Kotlin樣品
- Kotlin零基礎到進階
- 第一階段興趣入門
- kotlin簡介和學習方法
- 數據類型和類型系統
- 入門
- 分類
- val和var
- 二進制基礎
- 基礎
- 基本語法
- 包
- 示例
- 編碼規范
- 代碼注釋
- 異常
- 根類型“Any”
- Any? 可空類型
- 可空性的實現原理
- kotlin.Unit類型
- kotlin.Nothing類型
- 基本數據類型
- 數值類型
- 布爾類型
- 字符型
- 位運算符
- 變量和常量
- 語法和運算符
- 關鍵字
- 硬關鍵字
- 軟關鍵字
- 修飾符關鍵字
- 特殊標識符
- 操作符和特殊符號
- 算術運算符
- 賦值運算符
- 比較運算符
- 邏輯運算符
- this關鍵字
- super關鍵字
- 操作符重載
- 一元操作符
- 二元操作符
- 字符串
- 字符串介紹和屬性
- 字符串常見方法操作
- 字符串模板
- 數組
- 數組介紹創建及遍歷
- 數組常見方法和屬性
- 數組變化以及下標越界問題
- 原生數組類型
- 區間
- 正向區間
- 逆向區間
- 步長
- 類型檢測與類型轉換
- is、!is、as、as-運算符
- 空安全
- 可空類型變量
- 安全調用符
- 非空斷言
- Elvis操作符
- 可空性深入
- 可空性和Java
- 函數
- 函數式編程概述
- OOP和FOP
- 函數式編程基本特性
- 組合與范疇
- 在Kotlin中使用函數式編程
- 函數入門
- 函數作用域
- 函數加強
- 命名參數
- 默認參數
- 可變參數
- 表達式函數體
- 頂層、嵌套、中綴函數
- 尾遞歸函數優化
- 函數重載
- 控制流
- if表達式
- when表達式
- for循環
- while循環
- 循環中的 Break 與 continue
- return返回
- 標簽處返回
- 集合
- list集合
- list集合介紹和操作
- list常見方法和屬性
- list集合變化和下標越界
- set集合
- set集合介紹和常見操作
- set集合常見方法和屬性
- set集合變換和下標越界
- map集合
- map集合介紹和常見操作
- map集合常見方法和屬性
- map集合變換
- 集合的函數式API
- map函數
- filter函數
- “ all ”“ any ”“ count ”和“ find ”:對集合應用判斷式
- 別樣的求和方式:sumBy、sum、fold、reduce
- 根據人的性別進行分組:groupBy
- 扁平化——處理嵌套集合:flatMap、flatten
- 惰性集合操作:序列
- 區間、數組、集合之間轉換
- 面向對象
- 面向對象-封裝
- 類的創建及屬性方法訪問
- 類屬性和字段
- 構造器
- 嵌套類(內部類)
- 枚舉類
- 枚舉類遍歷&枚舉常量常用屬性
- 數據類
- 密封類
- 印章類(密封類)
- 面向對象-繼承
- 類的繼承
- 面向對象-多態
- 抽象類
- 接口
- 接口和抽象類的區別
- 面向對象-深入
- 擴展
- 擴展:為別的類添加方法、屬性
- Android中的擴展應用
- 優化Snackbar
- 用擴展函數封裝Utils
- 解決煩人的findViewById
- 擴展不是萬能的
- 調度方式對擴展函數的影響
- 被濫用的擴展函數
- 委托
- 委托類
- 委托屬性
- Kotlin5大內置委托
- Kotlin-Object關鍵字
- 單例模式
- 匿名類對象
- 伴生對象
- 作用域函數
- let函數
- run函數
- with函數
- apply函數
- also函數
- 標準庫函數
- takeIf 與 takeUnless
- 第二階段重點深入
- Lambda編程
- Lambda成員引用高階函數
- 高階函數
- 內聯函數
- 泛型
- 泛型的分類
- 泛型約束
- 子類和子類型
- 協變與逆變
- 泛型擦除與實化類型
- 泛型類型參數
- 泛型的背后:類型擦除
- Java為什么無法聲明一個泛型數組
- 向后兼容的罪
- 類型擦除的矛盾
- 使用內聯函數獲取泛型
- 打破泛型不變
- 一個支持協變的List
- 一個支持逆變的Comparator
- 協變和逆變
- 第三階段難點突破
- 注解和反射
- 聲明并應用注解
- DSL
- 協程
- 協程簡介
- 協程的基本操作
- 協程取消
- 管道
- 慕課霍丙乾協程筆記
- Kotlin與Java互操作
- 在Kotlin中調用Java
- 在Java中調用Kotlin
- Kotlin與Java中的操作對比
- 第四階段專題練習
- 朱凱Kotlin知識點總結
- Kotlin 基礎
- Kotlin 的變量、函數和類型
- Kotlin 里那些「不是那么寫的」
- Kotlin 里那些「更方便的」
- Kotlin 進階
- Kotlin 的泛型
- Kotlin 的高階函數、匿名函數和 Lambda 表達式
- Kotlin協程
- 初識
- 進階
- 深入
- Kotlin 擴展
- 會寫「18.dp」只是個入門——Kotlin 的擴展函數和擴展屬性(Extension Functions / Properties)
- Kotlin實戰-開發Android