背景
今天认识下 Kotlin 中的委托。
委托模式(Delegation pattern)
委托模式是指,两个对象参与处理同一个请求,接收请求的对象,将请求委托给另一个对象处理。
特点:
- 非继承。
- 便于基于现有类实现新的类,不用写重复的逻辑。
举例
Kotlin 代码例子:
interface IService {
fun hello()
fun print()
}
class ServiceImpl(val cookie: String): IService {
override fun hello() {
println("hello, I'm service")
}
override fun print() {
println("cookie bind to this service: $cookie")
}
}
class Derived(private val impl: IService): IService {
override fun hello() {
// 手动实现委托模式
impl.hello()
}
override fun print() {
// 手动实现委托模式
impl.print()
}
}
Kotlin 中的委托模式
Kotlin 很好的支持了委托模式,通关关键字 by
实现。可以划分为两种模式:类委托和属性委托。
类委托
以前面的例子为例,用 by
关键字实现类委托:
class Derived(private val impl: IService): IService by impl {
}
这样做,类 Derived
中,继承自接口 IService
的方法将会由对象 impl
的同名方法实现,而无需手动重载函数。效果等同于原来,同时大大简化了代码的复杂度。
如果类 Derived
中实现的方法,功能跟 impl
中的有所不同,可以通过重载修改。
class Derived(private val impl: IService): IService by impl {
override fun print() {
println("Derived::print")
}
}
值得注意,接口中的成员变量也能委托!但有一个特殊之处:委托的方法无法访问类中重载后的变量。
interface IService {
val name: String
fun print()
}
class ServiceBase(name_: String): IService {
override val name: String = name_
override fun print() {
println("ServiceBase($name)")
}
}
class Derived(base: IService): IService by base {
override val name: String = "Derived: ${base.name}"
}
fun main() {
val base = ServiceBase("base")
val derived = Derived(base)
derived.print() // 输出 ServiceBase(base)
println(derived.name) // 输出 Derived: base
}
属性委托
自定义委托
对属性主要有两个操作:读(get)和写(set)。
Kotlin 提供了可重载运算符 getValue()
和 setValue()
用于读写。
因此如果是要实现属性委托,只需要实现一个类,它里面重载了读写运算符。
class Demo {
private val str: String by Delegate()
}
class Delegate {
// 读
operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
return "$thisRef, thank you for delegating '${property.name}' to me!"
}
// 写
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
println("$value has been assigned to '${property.name}' in $thisRef")
}
}
属性被委托后,对属性的访问,将会委托到 Delegate()
的 getValue()
和 setValue()
上。
Kotlin 标准库中提供了 3 种接口用于自定义委托。
强烈建议使用标准库提供的接口实现自定义委托,能有效避免重载时把函数签名写错了!
ReadOnlyProperty
ReadWriteProperty
ObservableProperty
除了自定义委托以外,Kotlin 标准库中内置了一些委托类。
截止自 2022 年 4 月 5 日,Kotlin 标准库中提供了 4 种属性委托供使用。
lazy
懒加载委托属性,应该是最常用的一个。
lazy
本质上是一个方法,有两种,都需要接收一个函数块作为入参,用于委托属性的初始化工作。
public actual fun <T> lazy(initializer: () -> T): Lazy<T>
public actual fun <T> lazy(mode: LazyThreadSafetyMode, initializer: () -> T): Lazy<T>
用例:
class Demo {
private val mCount: Int by lazy {
1
}
}
特点:
- 仅当第一次访问时初始化,因而可以用于懒加载。
- 仅能用于
val
属性。 - 默认是线程安全的,但可以通过参数
mode
改变这一行为。
Delegates.observable
observable
如它名字,提供了一种监测属性值变换的能力。
public inline fun <T> observable(initialValue: T, crossinline onChange: (property: KProperty<*>, oldValue: T, newValue: T) -> Unit):
ReadWriteProperty<Any?, T>
用例:
class Demo {
private var mCount: Int by Delegates.observable { property, oldValue, newValue ->
println("property ${property.name} change from $oldValue to $newValue")
}
}
特点:
- 提供了属性值变化的监测能力
Delegates.vetoable
vetoable
和 observable
有些类似,可以监控属性值变化。此外,它还可用来限制属性值是否要改变。
public inline fun <T> vetoable(initialValue: T, crossinline onChange: (property: KProperty<*>, oldValue: T, newValue: T) -> Boolean):
ReadWriteProperty<Any?, T>
onChange
将会在写(setValue
)属性前调用,并且:
- 当它返回
false
时,对该属性的写将会失败,属性值保留原来的不变 - 当它返回
true
时,对该属性的写才生效。
用例:
class Demo {
private mCount: Int by Delegates.vetoable(0) { property, oldValue, newValue ->
if (newValue > 0) { // 仅当设置的值为正数是才允许设置
true
} else {
false
}
}
mCount = 1 // OK, mCount = 1
mCount += 1 // OK, mCount = 2
mCount -= 3 // Fail, mCount = 2
}
特点:
- 能控制属性的写入
Delegates.notNull
如其名,notNull
可以保证委托的属性不为 null
。
public fun <T : Any> notNull(): ReadWriteProperty<Any?, T>
用例:
class Demo {
private val mCount: Int? by Delegates.notNull<Int>()
private var mTotal: Int by Delegates.notNull<Int>()
}
它的用法跟 lateinit var
似乎有些类似,两者的区别在于:
lateinit var
不能用于原始类型(primitive type),如Int
、Long
;notNull
可以。
特点:
var
变量类型不能为null
。val
变量可以为null
。- 当未给委托属性赋值时,访问属性会抛出异常
IllegalStateException
。
委托给其他属性
一个属性的访问还可以委托另一个属性完成,通过 ::
。
通常,这种做法的场景是:给一个属性起别名。例如需要废弃某个 API 的属性,就可以这么做。
三类属性可以接受委托:
- 顶层属性
- 同一类中的成员属性或者扩展属性
- 其他类实例的成员属性或者扩展属性
示例:
var topLevelInt: Int = 0
class OtherClassDelegate(val otherClassInt: Int = 0)
class MyClass(var memberInt: Int = 0, val otherClass: OtherClassDelegate) {
var delegatedToTopLevel: Int by ::topLevelInt
var delegatedToMember: Int by this::memberInt
val otherClassInt: Int by otherClass::otherClassInt
}
var MyClass.extDelegated: Int by ::topLevelInt
更多技巧
自定义委托提供工厂
还有一种用法, 可以自定义委托构建工厂,用于提供委托类实例。
通常用于实现额外的复杂委托逻辑。
示例:
class DelegateFactory {
operator fun provideDelegate(thisRef: Any, property: KProperty<*>):
// 返回类型可以自定义
ReadWriteProperty<Any, String> {
}
}
顶层函数
如 lazy
,用顶层函数隐藏委托类的创造逻辑。
最佳实践
委托属性有一个技巧:通过将类的属性名和它的值放入一个 map
,然后类的属性在定义时,分别委托给这个 map
,能实现类的参数化赋值。
示例:
class User(map: Map<String, Any?>) {
val name: String by map
val age: Int by map
}
val user = User(mapOf(
"name" to "Kotlin",
"age" to 25
)
)