End

Kotlin 朱涛-9 委托 by 代理 懒加载 Delegate

本文地址


目录

09 | 委托:你为何总是被低估?

委托主要有两个应用场景,委托类委托属性

Jetpack Compose 中大量使用了委托特性,理解委托是理解 Jetpack Compose 的前提。

核心语法

  • 使用 var 修饰的属性,委托类中必须同时有使用关键字 operator 修饰的 getValuesetValue 方法
  • 方法 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 提供了几种标准委托,包括:

属性间的直接委托

可以直接在语法层面将属性 A 委托给属性 B

class Item {
    var count: Int = 0        // 和函数引用一样,【::count】代表的是属性的引用
    var total: Int by ::count // 把属性 total 的 get/set 方法委托给属性 count
}

fun main() {
    val item = Item()
    item.total = 5
    println("${item.total} - ${item.count}") // 5 - 5

    item.count = 6
    println("${item.total} - ${item.count}") // 6 - 6
}

可以发现,委托者和被委托者的数据是实时同步的。

懒加载委托 by lazy

val data: String by lazy { request() }                // 仅在首次访问时才会调用 request() 方法
fun request(): String = "网络数据".also { println("只会执行一次") }

fun main() {
    println(data)
    println(data)
}
  • 仅第一次访问 data 时,request() 才会执行
  • 后面再次访问 data 时,直接返回结果,request() 不会再执行

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
}

手写自定义属性委托

class QtStringDelegate(private var text: String) {
    operator fun getValue(thisRef: Man, property: KProperty<*>): String {
        println("getValue is called - ${property.name} - $property")
        return text // 不能在 getValue 中访问当前代理的属性,否则会因死循环触发 StackOverflowError
    }

    operator fun setValue(thisRef: Man, 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
}
  • 使用 var 修饰的属性,委托类中必须同时有使用关键字 operator 修饰的 getValuesetValue 方法
  • 上面 被委托属性 的类型是 String,被委托属性所属类的类型是 Man
  • 方法 getValue、setValue 中的 thisRef 的类型,必须是被委托属性所属类的类型,或其父类型
  • 方法 getValue 的返回值类型、setValue 的参数类型,必须是被委托属性的类型,或其父类型

接口自定义属性委托

通过实现接口 ReadWritePropertyReadOnlyProperty 自定义委托,可以让 IDEA 自动生成 getValue/setValue 方法的声明。

  • 通过实现 ReadOnlyProperty 接口为 val 属性自定义委托
  • 通过实现 ReadWriteProperty 接口为 var 属性自定义委托
class RWP(private var str: String) : ReadWriteProperty<Man, String> {
  override fun getValue(r: Man, p: KProperty<*>): String = str
  override fun setValue(r: Man, p: KProperty<*>, v: String) = let { str = v }
}

class ROP(private var str: String) : ReadOnlyProperty<Man, String> {
  override fun getValue(r: Man, p: KProperty<*>): String = str
}

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

posted @ 2016-12-12 16:55  白乾涛  阅读(5902)  评论(2编辑  收藏  举报