目录
09 | 委托:你为何总是被低估?
Kotlin 的委托主要有两个应用场景,一个是委托类
,另一个是委托属性
。
Jetpack Compose 中大量使用了 Kotlin 委托特性,理解委托是理解 Jetpack Compose 的前提。
核心语法:
- 使用
var
修饰的属性,委托类中必须同时有使用关键字operator
修饰的getValue
、setValue
方法 - 方法 getValue、setValue 中的
thisRef
的类型,必须是被委托属性所属类的类型
,或者其父类型
- 方法 getValue 的
返回值类型
、setValue 的参数类型
,必须是被委托属性的类型
,或者其父类型
委托类
委托类常常用于实现类的委托模式
。
interface DB { fun save() }
class SqlDB : DB { override fun save() = println("save to sql") }
class GreenDaoDB : DB { override fun save() = println("save to GreenDao") }
class UniversalDB(db: DB) : DB by db // 通过关键字 by 将接口的实现,委托给了对象 db
fun main() {
UniversalDB(SqlDB()).save()
UniversalDB(GreenDaoDB()).save()
}
这种委托模式在我们的实际编程中十分常见,UniversalDB 相当于一个壳,它虽然实现了 DB 这个接口,但并不关心它怎么实现。具体是用 SQL 还是 GreenDao,传不同的委托对象进去,它就会有不同的行为。
以上委托类的写法,等价于以下 Java 代码:
class UniversalDB implements DB {
private DB db;
public UniversalDB(DB db) { this.db = db; }
@Override public void save() { db.save(); } // 手动重写接口,将 save 委托给 db.save()
}
Kotlin 的委托类提供了语法层面的委托模式。通过这个 by 关键字,就可以自动将接口里的方法委托给一个对象
,从而可以帮我们省略很多接口方法适配的模板代码。
委托属性
Kotlin 委托类
委托的是接口方法
,而委托属性
委托的则是属性的 getter、setter
。
Kotlin 标准委托
Kotlin 提供了几种标准委托,包括:
- 两个属性之间的直接委托
- by lazy 懒加载委托
- Delegates.observable 观察者委托
- by map 映射委托
属性间的直接委托
从 Kotlin 1.4 开始,可以直接在语法层面将属性 A 委托给属性 B
:
class Item {
var count: Int = 0
var total: Int by ::count // 把属性 total 的 getter/setter 委托给属性 count
}
fun main() {
val item = Item()
item.total = 1
println("${item.total} - ${item.count}")
}
这里的::count
是属性的引用
,它跟函数引用
是一样的概念。
by lazy 懒加载委托
val data: String by lazy { // 实现属性的懒加载
request()
}
fun request(): String {
println("执行网络请求")
return "网络数据"
}
fun main() {
println(data)
println("----------")
println(data)
}
执行网络请求
网络数据
----------
网络数据
- 未访问 data 时,request() 不会被触发执行
- 第一次访问 data 时,request() 才会被触发执行
- 后面再次访问 data 时,直接返回结果,request() 也不会被触发执行
懒加载委托其实是一个高阶函数:
// 函数 lazy 的参数是一个高阶函数,返回值类型是接口 Lazy<T>,实际返回的是实现类 SynchronizedLazyImpl
public actual fun <T> lazy(initializer: () -> T): Lazy<T> = SynchronizedLazyImpl(initializer)
// 比上面的方法多了一个 enum 类型的参数
public actual fun <T> lazy(mode: LazyThreadSafetyMode, initializer: () -> T): Lazy<T> =
when (mode) {
LazyThreadSafetyMode.SYNCHRONIZED -> SynchronizedLazyImpl(initializer) // 多线程同步
LazyThreadSafetyMode.PUBLICATION -> SafePublicationLazyImpl(initializer)
LazyThreadSafetyMode.NONE -> UnsafeLazyImpl(initializer)
}
手写自定义委托
通过自定义委托
,可以根据自己的需求实现自己的属性委托。
import kotlin.reflect.KProperty
class StringDelegate(private var str: String) {
operator fun getValue(thisRef: Owner, property: KProperty<*>): String = "$str - fromDelegate"
operator fun setValue(thisRef: Owner, property: KProperty<*>, value: String) {
println("setValue is called")
str = "$value - setValue"
}
}
class Owner {
var text: String by StringDelegate("bqt") // 将 text 委托给 StringDelegate 的一个实例
}
fun main() {
val owner = Owner()
println(owner.text) // bqt - fromDelegate
owner.text = "xxx" // setValue is called
println(owner.text) // xxx - setValue - fromDelegate
}
- 使用
var
修饰的属性,委托类中必须同时有使用关键字operator
修饰的getValue
、setValue
方法 - 方法 getValue、setValue 中的
thisRef
的类型,必须是被委托属性所属类的类型
,或者其父类型
- 方法 getValue 的
返回值类型
、setValue 的参数类型
,必须是被委托属性的类型
,或者其父类型
接口自定义委托
如果觉得上面的写法太繁琐,也可以借助 Kotlin 提供的接口 ReadWriteProperty
、ReadOnlyProperty
,来自定义委托。通过实现接口,可以让 IntelliJ 帮我们自动生成 getValue
、setValue
方法的声明。
- 如果要为
val
属性自定义委托,就去实现ReadOnlyProperty
接口 - 如果要为
var
属性自定义委托,就去实现ReadWriteProperty
接口
import kotlin.properties.ReadWriteProperty
class StringDelegate(private var str: String) : ReadWriteProperty<Owner, String> {
override fun getValue(thisRef: Owner, property: KProperty<*>): String { // 省略了 operator,添加了 override
TODO("Not yet implemented")
}
override fun setValue(thisRef: Owner, property: KProperty<*>, value: String) {
TODO("Not yet implemented")
}
}
provideDelegate 嵌套委托
使用 provideDelegate,可以在属性委托之前
做一些额外的判断工作,例如可以根据委托属性的名字
做不同的处理逻辑。
手写法
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
class StringDelegate(private var str: String) : ReadWriteProperty<Owner, String> {
override operator fun getValue(thisRef: Owner, property: KProperty<*>): String = "$str - fromDelegate"
override operator fun setValue(thisRef: Owner, property: KProperty<*>, value: String) {
println("setValue is called")
str = "$value - setValue"
}
}
class SmartDelegator {
operator fun provideDelegate(thisRef: Owner, property: KProperty<*>): ReadWriteProperty<Owner, String> {
println("provideDelegate is called")
return if (property.name.contains("log")) StringDelegate("log-provideDelegate")
else StringDelegate("normal-provideDelegate")
}
}
class Owner {
var normalText: String by SmartDelegator() // 将 normalText 委托给 SmartDelegator 的一个实例
var logText: String by SmartDelegator() // 将 logText 委托给 SmartDelegator 的一个实例
}
接口法
上面的 SmartDelegator
中的 provideDelegate
方法,实际上是 kotlin.properties.PropertyDelegateProvider
接口中声明的方法,我们也可以通过实现这个接口来实现这个方法:
import kotlin.properties.PropertyDelegateProvider
class SmartDelegator : PropertyDelegateProvider<Owner, ReadWriteProperty<Owner, String>> {
override operator fun provideDelegate(thisRef: Owner, property: KProperty<*>): ReadWriteProperty<Owner, String> {...}
}
使用效果
fun main() {
val owner = Owner()
println("-----------------------")
println(owner.normalText)
println("-----------------------")
owner.logText = "xxx"
println(owner.logText)
}
provideDelegate is called
provideDelegate is called
-----------------------
normal-provideDelegate - fromDelegate
-----------------------
setValue is called
xxx - setValue - fromDelegate
可以看到,为了在委托属性的同时进行一些额外的逻辑判断,我们创建了一个 SmartDelegator,在其 provideDelegate
方法中,我们进行了一些逻辑判断,然后再把属性委托给 StringDelegate。
通过 provideDelegate
这样的方式,我们不仅可以嵌套
Delegator,还可以根据不同的逻辑派发
不同的 Delegator。
委托实战案例
属性可见性封装
对于某个成员变量 data,如果我们希望类的外部可以访问它的值,但不允许修改它的值,可以这样写:
class Model {
var data: String = ""
private set // 将属性的 set 方法声明为 private,这样类的外部就只能访问、而不能修改
}
然而,将上面 data 的类型从 String 变成集合
以后,问题就不一样了:
class Model {
val data: MutableList<String> = mutableListOf() // 定义成 var + private set 也一样有下面的问题
}
Model().data.add("World") // 类的外部仍然可以修改 data
对于集合而言,即使我们将其定义为只读变量 val
,仍然可以调用集合的 add()
方法修改它的值。可以利用两个属性之间的委托
语法解决这个问题。
class Model {
val data: List<String> by ::_data // 不可修改的集合,将其 getter 方法委托给私有的 _data
private val _data: MutableList<String> = mutableListOf() // 可变集合,外部无法直接访问
fun load() {
_data.add("Hello") // 类的内部可以访问【可变集合】,所以类的内部可以修改数据
}
}
fun main() {
Model().data[0] // 类的外部只可以访问【不可修改的集合】,所以类的外部只可以访问数据
}
- List 是 Kotlin 中不可修改的集合,它没有 add、remove 等方法
- MutableList 是 Kotlin 中的可变集合,它有 add、remove 方法
通过上面这种方式,我们就成功地将修改权保留在了类的内部。
数据与 View 的绑定
DataBinding
可用于对 Android 中的数据
与View
进行绑定,借助 Kotlin 的自定义委托属性
,也可以实现类似的功能。
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
class TextView {
var text: String? = ""
}
operator fun TextView.provideDelegate(value: Any?, property: KProperty<*>) = object : ReadWriteProperty<Any?, String?> {
override fun getValue(thisRef: Any?, property: KProperty<*>): String? = text // 内部类中可以访问外部类的成员
override fun setValue(thisRef: Any?, property: KProperty<*>, value: String?) {
text = value
}
}
- 首先为 TextView 定义了一个扩展函数
provideDelegate
- 这个扩展函数的返回值类型是
ReadWriteProperty
,泛型为<Any?, String?>
- 泛型
Any?
意味着,被委托属性所属类
的可以是任意类,也就是说:可以委托任意类 - 泛型
String?
意味着,被委托属性
只能是String?
类型(或者其父类型) - 类型
ReadWriteProperty
意味着,被委托属性可以是var
、也可以是val
- 总结来说就是:TextView 可以委托任意类的
String?
属性
- 泛型
val textView = TextView()
var message: String? by textView // 将 message 委托给 textView
textView.text = "Hello"
println(message) // Hello
message = "World"
println(textView.text) // World
ViewModel 委托
在 Android 当中,我们会经常用到 ViewModel 来存储界面数据。同时,我们不会直接创建 ViewModel 的实例,而对应的,我们会使用委托的方式来实现。
// MainActivity.kt
private val mainViewModel: MainViewModel by viewModels() // 将只读属性 mainViewModel 委托给了一个方法
viewModels()
的实现逻辑如下:
public inline fun <reified VM : ViewModel> ComponentActivity.viewModels( // 高阶函数
noinline factoryProducer: (() -> Factory)? = null // 参数是一个默认值为 null 的函数
): Lazy<VM> { // 返回值类型是 Lazy
val factoryPromise = factoryProducer ?: { defaultViewModelProviderFactory } // 如果参数为空...
return ViewModelLazy(VM::class, { viewModelStore }, factoryPromise) // 返回的是 Lazy 的一个实现类
}
public inline operator fun <T> Lazy<T>.getValue(thisRef: Any?, property: KProperty<*>): T = value // 扩展函数
- 首先为 ComponentActivity 定义了一个扩展函数
viewModels()
,所以才可以在 Activity 中直接调用这个方法 - 扩展函数
viewModels()
是一个高阶函数,参数是一个默认值为null
的函数 - 扩展函数
viewModels()
的返回值类型是Lazy
,根据委托语法,被委托类Lazy
必须要有getValue()
方法 - 由于在
Lazy
类的外部定义了一个扩展函数getValue()
,所以就满足委托的语法了
Android 官方这样的代码设计,就再一次体现了职责划分、关注点分离的原则。Lazy 类只包含核心的成员,其他附属功能,以扩展
的形式在 Lazy 外部提供。
小结
- 委托类,委托的是接口的方法,它在语法层面支持了
委托模式
- 委托属性,委托的是属性的 getter、setter,借助这个特性可以设计出非常复杂的代码
- Kotlin 官方还提供了几种标准的属性委托
- 两个属性之间的直接委托,在属性版本更新、可变性封装上,有着很大的用处
- by lazy 懒加载委托,可以让我们灵活地使用懒加载
- 自定义委托需要遵循 Kotlin 提供的一套语法规范
- 自定义委托时可以使用
provideDelegate
动态调整委托逻辑
2016-12-12