Kotlin 朱涛-9 委托 by 代理 Delegate
目录
09 | 委托:你为何总是被低估?
委托主要有两个应用场景,委托类
和 委托属性
。
Jetpack Compose 中大量使用了委托特性,理解委托是理解 Jetpack Compose 的前提。
核心语法
- 使用
var
修饰的属性,委托类中必须同时有使用关键字operator
修饰的getValue
、setValue
方法 - 方法 getValue/setValue 中
thisRef
的类型,必须是被委托属性所属类的类型
或其父类型,一般定义为Any?
- 方法 getValue 的
返回值类型
、setValue 的参数类型
,必须是被委托属性的类型
或其父类型,一般需明确指定
委托类 by
Kotlin 的委托类提供了语法层面的委托模式。通过 by 关键字,就可以自动将接口里的方法委托给一个对象,从而帮我们省略很多的模板代码。
用委托类实现委托模式
interface DB {
fun save()
}
class SqlDB : DB {
override fun save() = println("Sql")
}
class GreenDaoDB : DB {
override fun save() = println("GreenDao")
}
class QtDB(db: DB) : DB by db // 通过关键字 by 将 QtDB 委托给了对象 db
fun main() {
QtDB(SqlDB()).save() // Sql
QtDB(GreenDaoDB()).save() // GreenDao
}
注意:如果 QtDB 重写了 save()
方法,那么不管传入的 db 是哪个类的实例,都是执行的重写的 save()
方法。
等价的 Java 委托代码
以上委托类的写法,等价于以下 Java 代码:
class QtDB implements DB {
private DB db;
public QtDB(DB db) {
this.db = db;
}
@Override
public void save() {
db.save(); // 手动将 save() 方法委托给 db 对象
}
}
委托属性
Kotlin 委托类
委托的是接口的方法,而委托属性
委托的则是属性的 get/set 方法。
Kotlin 提供了几种标准委托,包括:
- 两个属性之间的直接委托
- 懒加载委托:by lazy
- 观察者委托:Delegates.observable
- 映射委托:by map
属性间的直接委托
可以直接在语法层面将属性 A 委托给属性 B
:
class Item {
var count: Int = 0 // 和函数引用一样,【::count】代表的是属性的引用
var total: Int by ::count // 把属性 total 的 get/set 方法委托给属性 count
val total2: Int by ::count // 把 total2 的 get 方法委托给属性 count
}
fun main() {
val item = Item()
item.total = 5
println("${item.total} - ${item.total2} - ${item.count}") // 5 - 5 - 5
item.count = 6
println("${item.total} - ${item.total2} - ${item.count}") // 6 - 6 - 6
}
可以发现,委托者和被委托者的数据是实时同步的。
懒加载委托 by lazy
val data: Long by lazy { request() } // 仅在首次访问时才会调用 request() 方法
fun request() = System.currentTimeMillis()
fun main() {
println(data)
println(data)
}
- 仅第一次访问 data 时,request() 才会执行
- 后面再次访问 data 时,直接返回结果,request() 不会再执行
lazy 的实现原理
lazy
其实是一个高阶函数
public actual fun <T> lazy(initializer: () -> T): Lazy<T>
public interface Lazy<out T> {
/**
* Gets the lazily initialized value of the current Lazy instance.
* Once the value was initialized, it must not change during the rest of lifetime of this Lazy instance.
*/
public val value: T
/**
* Returns `true` if a value for this Lazy instance has been already initialized, and `false` otherwise.
* Once this function has returned `true` it stays `true` for the rest of lifetime of this Lazy instance.
*/
public fun isInitialized(): Boolean
}
手写自定义属性委托
import kotlin.reflect.KProperty
class QtStringDelegate(private var text: String) {
operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
println("getValue is called - ${property.name} - $property")
return "[$text]" // 不能在 getValue 中访问当前代理的属性,否则会死循环
}
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
println("setValue is called - ${property.name} - $property")
text = value
}
}
class Man {
var job: String by QtStringDelegate("IT")
val age: String by QtStringDelegate("20")
}
fun main() {
val man = Man()
println(man.job) // getValue is called - job - var Man.job: kotlin.String
man.job = "Code" // setValue is called - job - var Man.job: kotlin.String
println(man.age) // getValue is called - age - val Man.age: kotlin.String
val bqt: String by QtStringDelegate("bqt")
println(bqt)
}
- 使用
var
修饰的属性,委托类中必须同时有使用关键字operator
修饰的getValue
、setValue
方法 - 上面
被委托属性
的类型是 String,被委托属性所属类
的类型是Any?
- 方法 getValue、setValue 中的
thisRef
的类型,必须是被委托属性所属类的类型,或其父类型 - 方法 getValue 的
返回值类型
、setValue 的参数类型
,必须是被委托属性的类型,或其父类型
接口自定义属性委托
通过实现接口 ReadWriteProperty
、ReadOnlyProperty
自定义委托,可以让 IDEA 自动生成 getValue/setValue
方法的声明。
- 通过实现
ReadOnlyProperty
接口为val
属性自定义委托 - 通过实现
ReadWriteProperty
接口为var
属性自定义委托
import kotlin.properties.ReadOnlyProperty
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
class RWP(private var text: String) : ReadWriteProperty<Man, String> {
override fun getValue(r: Man, p: KProperty<*>): String = "[$text]"
override fun setValue(r: Man, p: KProperty<*>, v: String) = let { text = v }
}
class ROP(private var text: String) : ReadOnlyProperty<Man, String> {
override fun getValue(r: Man, p: KProperty<*>): String = "[$text]"
}
class Man {
var job: String by RWP("IT")
val age: String by ROP("20")
}
嵌套属性委托
使用 provideDelegate
,可以在属性委托之前
做一些额外的判断工作,例如可以根据委托属性的名字
做不同的处理逻辑。
这样不仅可以嵌套
Delegator,还可以根据不同的逻辑派发
不同的 Delegator。
手写 provideDelegate - 1
class SmartDelegator {
operator fun provideDelegate(r: Man, p: KProperty<*>): QtStringDelegate {
return if (p.name == "job") QtStringDelegate("IT")
else QtStringDelegate("other-by-SmartDelegator")
}
}
class Man {
private val delegate = SmartDelegator()
var job: String by delegate // 将 job 和 age 委托给同一个 Delegator
val age: String by delegate
}
手写 provideDelegate - 2
class SmartDelegator2 {
operator fun provideDelegate(r: Man, p: KProperty<*>): ReadWriteProperty<Man, String> {
return if (p.name == "job") RWP("IT")
else RWP("other-by-SmartDelegator")
}
}
class Man {
private val delegate = SmartDelegator2()
var job: String by delegate
val age: String by delegate
}
使用 PropertyDelegateProvider
上面 provideDelegate
方法,实际上是 PropertyDelegateProvider
接口中声明的方法:
class SmartDelegator3 : PropertyDelegateProvider<Man, ReadWriteProperty<Man, String>> {
override operator fun provideDelegate(r: Man, p: KProperty<*>): ReadWriteProperty<Man, String> = RWP("xx")
}
案例:管控集合数据的修改权
问题背景
对于某个成员变量,如果我们希望类的外部仅可以访问它的值,但不允许修改它的值,可以将属性的 set
方法声明为 private
,或者声明为只读变量 val
。
class Model {
val data1: String = "yy" // 外部和内部都是只能访问、不能修改
var data2: String = "xx"
private set // 外部只能访问、不能修改,而类的内部既能访问、也能修改
}
然而,对于集合而言,即使将其声明为 val
,仍然可以调用其 add/remove
等方法修改它的数据。
class Model {
var list1: MutableList<String> = mutableListOf()
private set
val list2: MutableList<String> = mutableListOf()
}
fun main() {
Model().list1.add("bqt")
Model().list2.add("bqt")
}
解决方案
可以利用两个属性之间的委托
语法解决这个问题。
class Model {
val data: List<String> by ::_data // 不可修改的集合,对外暴露
private val _data: MutableList<String> = mutableListOf() // 可修改的集合,仅对内暴露
fun load(string: String) { // 对外暴露一个修改集合的方法
_data.add(string) // 实际上操作的是仅对内暴露的可变集合
}
}
fun main() {
val model = Model()
model.load("bqt") // 外部【只】可以通过暴露的方法修改集合,而不能直接修改
println(model.data[0]) // 外部可以正常访问集合的数据
}
通过这种方式,我们就成功地将集合数据的修改权,保留在了类的内部。
案例:数据绑定 DataBinding
借助自定义委托属性
,可以实现类似 DataBinding
框架中数据与 View 进行绑定的功能。
class TextView {
var text: String? = null // 模拟 Android View 中需要被绑定的数据
}
注意,因为不能修改 TextView 的源码,所以只能通过扩展、而非实现接口的方式,让 TextView 支持属性委托
方案一
operator fun TextView.getValue(r: Any?, p: KProperty<*>): String? = text
operator fun TextView.setValue(r: Any?, p: KProperty<*>, v: String?) = let { text = v }
- 类型
Any?
意味着,被委托属性所属类
的可以是任意类,当然被委托属性
没有所属的类也是可以的 - 类型
String?
意味着,被委托属性
只能是String?
类型(或者其父类型) - 我们同时给 TextView 扩展了 getValue/setValue 方法,所以
被委托属性
既可以是var
、也可以是val
注意,将
被委托属性所属类
声明为 Any? 是此方案的核心!
方案二
operator fun TextView.provideDelegate(r: Any?, p: KProperty<*>): ReadWriteProperty<Any?, String?> {
return object : ReadWriteProperty<Any?, String?> {
override fun getValue(r: Any?, p: KProperty<*>): String? = text
override fun setValue(r: Any?, p: KProperty<*>, v: String?) = let { text = v }
}
}
测试代码
fun main() {
val tv = TextView()
var message: String? by tv // 将一个变量和 textView 进行绑定,此后他们的值将会【实时同步】
tv.text = "Hello"
println(message) // Hello
message = "World"
println(tv.text) // World
}
小结
- 委托类,委托的是
接口的方法
,它在语法层面支持了委托模式
- 委托属性,委托的是
属性的 get/set
,借助这个特性可以设计出非常复杂的代码 - Kotlin 官方提供了几种标准的属性委托
- 两个属性之间的直接委托,在属性版本更新、可变性封装上,有着很大的用处
- by lazy 懒加载委托,可以让我们灵活地使用懒加载
- 自定义委托需要遵循 Kotlin 提供的一套语法规范
- 自定义委托时可以使用
provideDelegate
动态调整委托逻辑
2016-12-12
本文来自博客园,作者:白乾涛,转载请注明原文链接:https://www.cnblogs.com/baiqiantao/p/6165143.html