zoukankan      html  css  js  c++  java
  • Kotlin 朱涛9 委托 代理 懒加载 Delegate

    本文地址


    目录

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

    Kotlin 的委托主要有两个应用场景,一个是委托类,另一个是委托属性

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

    核心语法:

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

    属性间的直接委托

    从 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 修饰的 getValuesetValue 方法
    • 方法 getValue、setValue 中的 thisRef 的类型,必须是被委托属性所属类的类型,或者其父类型
    • 方法 getValue 的返回值类型、setValue 的参数类型,必须是被委托属性的类型,或者其父类型

    接口自定义委托

    如果觉得上面的写法太繁琐,也可以借助 Kotlin 提供的接口 ReadWritePropertyReadOnlyProperty,来自定义委托。通过实现接口,可以让 IntelliJ 帮我们自动生成 getValuesetValue 方法的声明。

    • 如果要为 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

  • 相关阅读:
    修改MySQL表varchar字段的小实验
    MySQL5.6同步指定数据库
    Redis安装
    MongoDB安装
    RMAN备份注意事项
    Oracle Data Guard Protection Modes
    Linux Shell 文本处理工具
    ORACLE 收缩表空间的数据文件
    crfclust.bdb导致磁盘满
    undo管理
  • 原文地址:https://www.cnblogs.com/baiqiantao/p/6165143.html
Copyright © 2011-2022 走看看