- Published on
Kotlin Scope Functions:用兩個問題選對函式

在 Kotlin 的各種範例程式碼中,let、run、apply、also、with 幾乎無所不在。功能看起來非常相近,某些情況下甚至可以互換,讓人難以判斷該選哪一個
什麼是 Scope Function?
Scope Function 可以理解為「作用域函式」,讓我們可以在某個物件的範圍內執行一段程式碼,不需要重複寫變數名稱
假設有一個 Person 物件
val person = Person()
person.name = "Cash"
person.age = 30
person.city = "Taichung"
println(person)
用 Scope Function 改寫後
val person = Person().apply {
name = "Cash"
age = 30
city = "Taichung"
}.also {
println(it)
}
兩個關鍵差異
理解這五個函式,核心差異只有兩個維度
差異一:用 this 還是 it 引用物件?
this(lambda receiver):可以直接呼叫物件的屬性和方法,不用加前綴,就像「進入」了那個物件裡面it(lambda argument):需要透過it(或自訂名稱)來引用物件,就像拿到了一個參數
// this → 直接存取,不用前綴
person.run {
println(name) // 等同於 this.name
println(age) // 等同於 this.age
}
// it → 透過 it 來引用
person.let {
println(it.name)
println(it.age)
}
差異二:回傳物件本身,還是 lambda 的執行結果?
- 回傳物件本身:方便做鏈式呼叫,適合「設定完繼續用」的場景
- 回傳 lambda 結果:適合轉換或計算的場景
// 回傳物件本身 → 後面可以繼續接著用
val person = Person().apply {
name = "Cash"
}
// person 是 Person 物件
// 回傳 lambda 結果 → 回傳最後一行的值
val nameLength = person.let {
it.name.length
}
// nameLength 是 Int
五個函式對照
| 引用方式 | 回傳值 | 適合場景 | |
|---|---|---|---|
let | it | lambda 結果 | null 安全處理、轉換 |
run | this | lambda 結果 | 物件運算、取得結果 |
apply | this | 物件本身 | 物件初始化、配置 |
also | it | 物件本身 | 附加的副作用 |
with | this | lambda 結果 | 對物件做多次操作 |
矩陣來看更直觀
| 回傳 lambda 結果 | 回傳 物件本身 | |
|---|---|---|
| this | run | apply |
| it | let | also |
with 稍後單獨說明
要死記嗎?其實不用
很多初學者看到這五個函式會覺得「名字都很抽象,像在背單字」,但它們其實有語意,命名也對應了 Kotlin 標準函式庫的設計目的
可以先把它們當成「英文動作 + 回傳值」來記
| 函式 | 名稱語感 | 設計重點 |
|---|---|---|
let | 讓這個值去做一件事 | 把物件當參數傳進 lambda,回傳結果 |
run | 執行一段程式碼 | 在物件作用域中執行,回傳結果 |
apply | 套用設定 | 在物件作用域中設定,回傳物件本身 |
also | 另外順便做一件事 | 以參數形式做副作用,回傳物件本身 |
with | 帶著某個物件一起做事 | 不是 extension function,回傳結果 |
如果你只想先記最實用的版本,可以用這句口訣
- 改設定並保留物件:
apply - 順便做副作用並保留物件:
also - 做轉換拿結果:
let/run
換句話說,不是死記,而是先理解每個動詞在語意上「想做什麼」,再用 this/it 與回傳值去收斂選擇
逐一介紹
let — 拿到物件,做點事,回傳結果
public inline fun <T, R> T.let(block: (T) -> R): R
用 it 引用物件,回傳 lambda 結果
最常見的用法是搭配 ?. 做 null 安全處理
val name: String? = findUserName()
// ❌ 傳統寫法
if (name != null) {
println(name.length)
}
// ✅ 用 let
name?.let {
println(it.length)
}
?.let 的意思是「如果不是 null,就拿它來做某件事」,是 Kotlin 中最常見的慣用寫法之一
也可以用於轉換
val numbers = listOf(1, 2, 3)
val firstDoubled = numbers.first().let { it * 2 }
// firstDoubled = 2
lambda 內的邏輯有多行時,建議用自訂名稱取代 it,可讀性會明顯提升
name?.let { userName ->
println("Hello, $userName")
}
run — 進入物件裡面,做完事,回傳結果
public inline fun <T, R> T.run(block: T.() -> R): R
用 this 引用物件,回傳 lambda 結果
存取多個屬性並回傳計算結果時很好用
val greeting = person.run {
// 直接用 name,不用寫 it.name 或 person.name
"Hello, I'm $name, $age years old, from $city"
}
同樣支援 null 安全
val result = person?.run {
"$name ($age)"
} ?: "Unknown"
run 也有一個不需要物件的版本,可以建立一個區域作用域
val hexColor = run {
val red = 0xFF
val green = 0xA5
val blue = 0x00
"#${red.toString(16)}${green.toString(16)}${blue.toString(16)}"
}
apply — 進入物件裡面做設定,回傳物件本身
public inline fun <T> T.apply(block: T.() -> Unit): T
用 this 引用物件,回傳物件本身
apply 是「套用設定」,設定完把物件還給你。就像一個簡易的 Builder Pattern
val person = Person().apply {
name = "Cash"
age = 30
city = "Taichung"
}
Android 開發中很常見
val intent = Intent(this, DetailActivity::class.java).apply {
putExtra("id", userId)
putExtra("name", userName)
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
測試的 Arrange 階段也適合
val testUser = User().apply {
id = 1
name = "Test User"
email = "[email protected]"
isActive = true
}
also — 順便做一件事,物件原封不動還給你
public inline fun <T> T.also(block: (T) -> Unit): T
用 it 引用物件,回傳物件本身
also 的意思是「還有,順便做一件事」,不影響鏈式呼叫的流程。最常用在加入 log 或驗證
val numbers = mutableListOf(1, 2, 3)
.also { println("初始清單: $it") }
.apply { add(4) }
.also { println("加入元素後: $it") }
輸出
初始清單: [1, 2, 3]
加入元素後: [1, 2, 3, 4]
fun createUser(name: String): User {
return User(name).also {
requireNotNull(it.name) { "名稱不能為空" }
println("建立使用者: ${it.name}")
}
}
with — 把物件帶進來,做多次操作
public inline fun <T, R> with(receiver: T, block: T.() -> R): R
用 this 引用物件,回傳 lambda 結果。with 不是 extension function,而是普通函式,需要把物件當參數傳入
val message = with(person) {
"Hello, I'm $name, $age years old"
}
和 run 幾乎相同
// 這兩種寫法效果完全相同
with(person) { "$name, $age" }
person.run { "$name, $age" }
with 越來越少被推薦使用,原因有幾個
null 安全呼叫行不通。with 接受的是普通參數,不支援 ?. 語法
val person: Person? = findPerson()
// ❌ with 無法這樣寫
with(person) { // person 為可空型別,無法安全存取屬性
println(name)
}
// ✅ run 可以輕鬆處理
person?.run {
println(name)
}
鏈式呼叫也不合用。with 是普通函式,物件用參數傳入,無法接在呼叫鏈中使用
// ❌ 不順暢
with(getUser()) {
// ...
}
// ✅ 鏈式風格更自然
getUser().run {
// ...
}
run 能做到 with 的所有事情,還多了 null 安全和鏈式呼叫的優勢。團隊統一使用 run 取代 with,需要記憶的 API 少一個,程式碼風格也更一致
實務選擇
兩個問題就能定位到對的函式
- 需要回傳物件本身,還是計算結果? → 物件本身用
apply/also,結果用let/run - 比較想用
it.xxx還是直接用xxx? →it用let/also,直接用就選run/apply
需要配置物件,回傳物件本身? → apply
需要做 log/驗證等副作用,回傳物件本身? → also
需要 null 安全處理或轉換? → let
需要用 this 存取屬性並回傳結果? → run
注意事項
不要過度嵌套
// ❌ 嵌套太深,可讀性變差
person?.let { p ->
p.address?.let { addr ->
addr.city?.let { city ->
println(city)
}
}
}
// ✅ 展開比較好讀
val city = person?.address?.city
if (city != null) {
println(city)
}
Scope Function 是為了讓程式碼更好讀,嵌套之後反而更難讀的話就不要用
避免在 Scope Function 中修改外部變數
// ❌ 修改外部變數,容易造成 side effect
var result = ""
person.apply {
result = name
}
// ✅ 讓回傳值做事
val result = person.run {
name
}
lambda 超過 3-5 行就考慮抽成方法
// ❌ lambda 內做太多事
val user = User().apply {
name = "Cash"
age = 30
email = "[email protected]"
role = findDefaultRole()
permissions = calculatePermissions(role)
lastLogin = LocalDateTime.now()
validateAndSave()
}
// ✅ 複雜邏輯抽成方法
val user = createDefaultUser("Cash", 30, "[email protected]")
小結
apply、also、let、run 涵蓋了絕大多數使用場景。with 能做的事,run 都可以取代,而且更有彈性,忽略掉它也沒什麼損失
圖片來源:AI 產生