Kotlin 委派 (Delegation)
委派 (Delegation) 是一種設計模式,將某個物件的工作「外包」給另一個物件處理。
Kotlin 原生支援了 Class Delegation 和 Property Delegation,讓這種模式變得超級簡單,核心關鍵字就是 by。
Class Delegation (類別委派)
這是一種取代「繼承」的好方法 (Composition over Inheritance)。
假設你想實作一個 List,但只想修改其中幾個方法的行為,其他都照舊。
interface Base {
fun print()
}
class BaseImpl(val x: Int) : Base {
override fun print() = println(x)
}
// Derived 類別實作 Base 介面,但它把所有工作都「委派」給 b 這個物件
class Derived(b: Base) : Base by b
fun main() {
val b = BaseImpl(10)
Derived(b).print() // 輸出 10
}
你不需要手動寫 override fun print() { b.print() },編譯器幫你做掉了。
Property Delegation (屬性委派)
這是 Kotlin 最神奇的功能之一。
你可以把屬性的 get() 和 set() 邏輯外包給一個代理物件。
語法:val/var <property name>: <Type> by <expression>
延遲初始化 lazy
最常用的標準委派。變數只會在 第一次被存取時 才執行初始化程式碼。
val heavyData: String by lazy {
println("Computing...")
// 模擬耗時操作
Thread.sleep(1000)
"Result"
}
fun main() {
println("Start")
println(heavyData) // 印出 "Computing..." 然後 "Result"
println(heavyData) // 直接印出 "Result" (不會再計算)
}
觀察者 observable
當屬性值發生改變時,會自動執行 callback。
var name: String by Delegates.observable("Initial") { prop, old, new ->
println("$old -> $new")
}
fun main() {
name = "Miko" // 輸出 Initial -> Miko
name = "Jason" // 輸出 Miko -> Jason
}
Map 委派 map
可以直接用 Map 來儲存屬性值 (常用於解析 JSON 或動態設定)。
class User(val map: Map<String, Any?>) {
val name: String by map
val age: Int by map
}
val user = User(mapOf(
"name" to "John Doe",
"age" to 25
))
println(user.name) // John Doe
否決賦值 vetoable
如果你希望在指派屬性新值之前進行檢查,決定是否接受這個新值,可以使用 vetoable。它類似 observable,但 Callback 必須回傳一個 Boolean。如果回傳 true,新值才會被接受;回傳 false 則維持原值。
var max: Int by Delegates.vetoable(0) { prop, old, new ->
new > old // 只有當新值大於舊值時才更新
}
fun main() {
println(max) // 0
max = 10
println(max) // 10 (10 > 0,更新成功)
max = 5
println(max) // 10 (5 < 10,更新被否決,維持原值)
}
非空委派 notNull
有些屬性因為某些原因無法在建構式中初始化(例如需要依賴外部注入),但你又不希望它宣告為 Nullable (Type?)。這時候可以使用 Delegates.notNull()。
它類似 lateinit,但 notNull 實作上是利用委派,可以用於 Primitive Types (Int, Double 等),而 lateinit 不行。
var config: String by Delegates.notNull()
fun main() {
// println(config) // 如果在賦值前讀取,會拋出 IllegalStateException
config = "Loaded"
println(config) // Loaded
}
Custom Delegation (自定義委派)
如果標準庫提供的委派不滿足需求,你也可以自己寫!
只要一個類別實作了 operator fun getValue (如果是 var 屬性則還需要 setValue),它就可以當作委派物件。
步驟 1:建立委派類別
實作 ReadOnlyProperty 或 ReadWriteProperty 介面,不僅讓程式碼更標準,IDE 也會提供型別檢查輔助。
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
class TrimDelegate : ReadWriteProperty<Any?, String> {
private var trimmedValue: String = ""
override fun getValue(thisRef: Any?, property: KProperty<*>): String {
return trimmedValue
}
override fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
// 自動去除頭尾空白
trimmedValue = value.trim()
println("Set value to '$trimmedValue'")
}
}
步驟 2:使用它
class Post {
var content: String by TrimDelegate()
}
fun main() {
val post = Post()
post.content = " Hello World! "
println("Content: '${post.content}'")
// 輸出:
// Set value to 'Hello World!'
// Content: 'Hello World!'
}
這在封裝重複的 Getter/Setter 邏輯(如格式化、驗證、Log 記錄)時非常強大。
總結
- by 關鍵字:一切委派的核心。
- by lazy:最常用,省資源神器。
- Delegation Pattern:用組合取代繼承,降低耦合。