Kotlin学习系列之:委托(Delegate)

1. 引入:委托作为一种传统的设计模式,在Java中要想实现这种设计模式,就需要自己进行类的结构设计来实现。而在Kotlin中,提供语言层面上的支持,我们可以通过by关键字很轻松就能实现。

2. 类委托(class delegate)

  • 自己动手实现委托:

    interface ServerApi {
        fun login(username: String, password: String)
    }
    
    
    class Retrofit : ServerApi {
    
        /*类比登录操作
        */
        override fun login(username: String, password: String) {
            println("login successfully.")
        }
    }
    
    class RemoteRepository : ServerApi {
    
        private val serverApi: ServerApi = Retrofit()
    
        override fun login(username: String, password: String) {
            serverApi.login(username, password)
        }
    }
    
    fun main() {
    
        val repository = RemoteRepository()
        repository.login("David", "123456") //输出 login successfully.
    }
    

    首先我们声明了一个接口ServerApi,然后定义了其两个实现类:Retrofit、RemoteRepository,并且我们在Retrofit类里实现了具体的login方法,而在RemoteRepository里对于login方法的实现逻辑就直接委托给了serverApi(Retrofit对象),这样我们就自己实现了一个委托模式。但是我们发现:无论我们在ServerApi中定义多少个抽象方法,RemoteRepository类的结构是有规律可言的,或者不礼貌地说这些代码比较冗余。那下面我们就来看看如何通过by关键字轻松实现类委托。

  • 使用by关键字,改写RemoteRepository类:

    class RemoteRepository(retrofit: Retrofit) : ServerApi by retrofit

    搞定。可以看出语法就是:在要实现的接口后面 + by + 委托对象。这样我们使用了Kotlin的类委托。

  • 透过现象看本质:

    按照惯例,我们现在去反编译,从字节码层面上去理解Kotlin的类委托。实际上大家可以去猜测一下底层实现(应该和前面我们手动实现的一样):

    的确,和我们之前自己手动实现的一样:编译器会自动在被委托类添加了一个委托类对象作为它的属性,并且在构造方法中将我们指定的委托对象赋值给了它,然后实现了抽象方法,实现的逻辑就是委托给这个添加的委托类对象。

  • 对于被委托类中某些方法,可以提供自己的实现

    interface ServerApi {
        fun login(username: String, password: String)
        fun register(username: String, password: String)
    }
    
    class Retrofit : ServerApi {
    
        override fun login(username: String, password: String) {
            println("login: username = $username, password = $password")
        }
    
        override fun register(username: String, password: String) {
            println("register: username = $username, password = $password")
        }
    }
    
    class RemoteRepository(retrofit: Retrofit) : ServerApi by retrofit{
    
        override fun register(username: String, password: String) {
            println("register in RemoteRepository.")
        }
    }
    

    这样的话,对于register的调用,就会执行自己的逻辑,编译器就不会再为你提供实现。

  • 多个委托:

    interface NewsApi {
        fun getNewsList()
    }
    
    class NewsApiImpl : NewsApi {
        override fun getNewsList() {
            println("NewsApiImpl: getNewsList()")
        }
    }
    
    class RemoteRepository(retrofit: Retrofit) : ServerApi by retrofit, NewsApi by NewsApiImpl()
    

    如果需要多个委托,采用这种语法就可以,一一对应。

3. 属性委托(delegated property)

  • 引入:在kotlin中,不光光支持类委托,对于属性的访问(set、get),我们也可以指定它的委托
  • 如何实现属性委托:

    class Delegate {
    
        operator fun getValue(thisRef: Any?, property: KProperty<*>) = "hello world"
    
        operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
            println("the new value is $value")
        }
    }
    
    class RealSub {
        var str: String by Delegate()
    }
    
    fun main() {
        val sub = RealSub()
        sub.str = "hello"   
        println(sub.str)
    }
    

    实际上可以分为两步:

    a. 定义一个属性委托类(如这里的Delegate),然后在这个类中提供两个方法:getValue()、setValue(),他们的方法签名必须按照如下格式:

    operator fun getValue(thisRef: Any?, property: KProperty<*>): T {}
    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {} 
    

    operator关键字:表示运算符重载,可参考此篇文章
    第一个参数:表示被委托属性所属的对象
    第二个参数:类似Java中的Field,属性的反射对象
    第三个参数(setValue中):被委托属性对象要赋的值
    返回值(getValue中):被委托属性对象的值

    b. 在被委托的属性(如这里的str)后添加:by 委托对象

    我们再来看main方法的测试代码:

    sub.str = "hello",就会触发setValue方法的调用,打印:the new value is hello
    println(sub.str),就会触发getValue方法的调用,返回"hello world", 故打印:hello world

    总结来说,属性委托就是对于某属性的访问,委托给了我们指定的委托对象中的getValue、setValue(var类型属性)方法。刚刚我们是自己实现了这个了属性委托,实际上Kotlin标准库中也为了提供几种常用的属性委托,能够为我们的开发带来极大的便利。

4. 标准库中的属性委托之:lazy

  • 使用场景:延迟val属性的初始化时机(第一次访问的时候才会去初始化)
  • 示例代码:

    class LazyTest {
        val lazyValue: String by lazy {
            println("I'm in lazy.")
            "lazyValue"
        }
    }
    
    fun main() {
    
        val lazyTest = LazyTest()
        println(lazyTest.lazyValue)
        println(lazyTest.lazyValue)
    }   
    

    输出结果为:

    I'm in lazy.
    lazyValue
    lazyValue
    

    下面我们来解释一下这段代码,从而理解lazy委托的特点。在by关键字后跟上一个lazy+lambda表达式,那么这个lazy是个啥:

    public actual fun <T> lazy(initializer: () -> T): Lazy<T> = SynchronizedLazyImpl(initializer)
    

    actual关键字可以先忽略,它与多平台相关。忽略后,可以看到这个lazy就是个普通的function,返回一个Lazy对象,那么也就是说返回的这个Lazy对象作为我们lazyValue属性的委托对象。再看这个Lazy:

    public inline operator fun <T> Lazy<T>.getValue(thisRef: Any?, property: KProperty<*>): T = value
    

    定义了一个扩展方法:getValue,这个getValue方法签名对我们来说应该很熟悉了。同时我们还可以得出一个结论,对于委托类,除了以member的形式来定义getValue/setValue,还可以通过extension的方式来定义

    最后注意一下lazy函数的参数,一个不带参数、返回一个T类型的lambda表达式(如果lambda表达式是function的最后一个参数,那么推荐将其写到外面)。这个lambda表达式的返回值就是我们要赋给属性的值。

    上面仅仅是对lazy委托本身进行了分析,那么它有什么特点呢?我们还得结合main方法中的测试代码来看:

    当第一次访问lazyValue时,打印出了:

    I'm in lazy.
    lazyValue.
    

    当第二次访问lazyValue时,仅打印了:

    lazyValue.
    

    可以看出,lazy后的lambda表达式只是在被委托属性第一次被访问的时候执行了一次,并且将返回值用来初始化了被委托属性,之后对于被委托属性的访问,直接使用初始值。这里说的访问,确切地说是get()。

  • 如果你对多线程编程敏感的话,可以隐约意识到,在多线程环境下,这里会不会出现多线程同步的问题,毕竟lambda表达式里不是原子操作。不慌,我们来看lazy的另一个重载方法:

    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)
        }
    

    它会多一个参数,LazyThreadSafetyMode类型mode,LazyThreadSafetyMode实际上是一个枚举类,它有三个枚举对象:

    LazyThreadSafetyMode.SYNCHRONIZED: 这种模式下,lambda表达式中的代码是加了锁的,确保只有一个线程能够来执行初始化
    LazyThreadSafetyMode.PUBLICATION: 这种模式下,lambda表达式中的代码是允许多个线程同时访问的,但是只有第一个返回的值作为初始值
    LazyThreadSafetyMode.NONE: 这种模式下,lambda表达式中的代码在多线程的情况下的行为是不确定的,这种模式并不推荐使用,除非你确保它永远不可能在多线程环境下使用,然后使用这种模式可以避免因锁所带来的额外开销。

    很庆幸,默认情况下,lazy的模式是第一种,所以默认情况下是不会出现同步问题的。

5. 标准库的属性委托之:observable/vetoable

  • 特点:可以对属性值的变化进行监听
  • 示例代码:

    class Person {
        var name: String by Delegates.observable("<no name>") { property, oldValue, newValue ->
            println("property'name is ${property.name}, oldValue = $oldValue, newValue = $newValue")
        }
    }
    
    fun main() {
        val person = Person()
        person.name = "Alice"
        person.name = "Bob"
    }
    

    我们关注by后面的部分就可以了,调用了Delegates.observable(),将它的返回值作为委托对象:

    public object Delegates {
    
        public inline fun <T> observable(initialValue: T, crossinline onChange: (property: KProperty<*>, oldValue: T, newValue: T) -> Unit):
                ReadWriteProperty<Any?, T> =
            object : ObservableProperty<T>(initialValue) {
                override fun afterChange(property: KProperty<*>, oldValue: T, newValue: T) = onChange(property, oldValue, newValue)
            }
    }
    

    Delegates是个对象,observable接收两个参数:一个初始值,赋给被委托属性;一个lambda表达式,lambda有三个回调参数,描述属性的KProperty、旧值以及新值。一旦被委托属性的值发生变化(即调用set方法)时,就会回调lambda表达式。

    现在再来看main函数中的代码就简单多了:

    person.name = "Alice" => 打印:

    property'name is name, oldValue = <no name>, newValue = Alice
    

    person.name = "Bob" => 打印:

    property'name is name, oldValue = Alice, newValue = Bob
    

    回过头再来关注一下这个observable方法的返回值类型:ReadWriteProperty

    /**
     * Base interface that can be used for implementing property delegates of read-write properties.
     *
     * This is provided only for convenience; you don't have to extend this interface
     * as long as your property delegate has methods with the same signatures.
     *
     * @param R the type of object which owns the delegated property.
     * @param T the type of the property value.
     */
    public interface ReadWriteProperty<in R, T> {
        /**
         * Returns the value of the property for the given object.
         * @param thisRef the object for which the value is requested.
         * @param property the metadata for the property.
         * @return the property value.
         */
        public operator fun getValue(thisRef: R, property: KProperty<*>): T
    
        /**
         * Sets the value of the property for the given object.
         * @param thisRef the object for which the value is requested.
         * @param property the metadata for the property.
         * @param value the value to set.
         */
        public operator fun setValue(thisRef: R, property: KProperty<*>, value: T)
    }
    

    在这个接口中,我们看到了有熟悉方法签名的getValue、setValue方法。一起来读一下这个接口的文档注释:

    它是一个用来对可读可写(即var)的属性实现属性委托的接口;但是它的存在仅仅是为了方便,只要我们的属性委托拥有相同的方法签名,开发者不必来继承这个接口。

    与之类似的还有个ReadOnlyProperty:

    /**
     * Base interface that can be used for implementing property delegates of read-only properties.
     *
     * This is provided only for convenience; you don't have to extend this interface
     * as long as your property delegate has methods with the same signatures.
     *
     * @param R the type of object which owns the delegated property.
     * @param T the type of the property value.
     */
    public interface ReadOnlyProperty<in R, out T> {
        /**
         * Returns the value of the property for the given object.
         * @param thisRef the object for which the value is requested.
         * @param property the metadata for the property.
         * @return the property value.
         */
        public operator fun getValue(thisRef: R, property: KProperty<*>): T
    }
    

    注释基本同ReadWriteProperty类似,只不过它是服务于val属性。

    同observable委托有相同功能的还有一个:vetoable。

     public inline fun <T> vetoable(initialValue: T, crossinline onChange: (property: KProperty<*>, oldValue: T, newValue: T) -> Boolean):
        ReadWriteProperty<Any?, T> =
    object : ObservableProperty<T>(initialValue) {
        override fun beforeChange(property: KProperty<*>, oldValue: T, newValue: T): Boolean = onChange(property, oldValue, newValue)
    }
    

    发现它的lambda会要求一个返回值,这个返回值有什么作用呢?这与observable和vetoable的回调时机不同有关:observable的回调时机是在属性值修改之后,vetoable的回调时机在属性值被修改之前。如果返回值为true,属性值就会被修改成新值;如果返回值为false,此次修改就会直接被丢弃。

    我们来看示例代码:

    class Adult {
    
        var age: Int by Delegates.vetoable(18) { property, oldValue, newValue ->
            println("property'name is ${property.name}, oldValue = $oldValue, newValue = $newValue")
            newValue >= 18
        }
    }
    
    fun main(){
        val adult = Adult()
        adult.age = 25
        println("adult.age = ${adult.age}")
        adult.age = 16
        println("adult.age = ${adult.age}")
    }
    

    当adult.age = 25时,属性值被成功修改;adult.age = 16,修改操作被丢弃,修改失败,属性值还是原来的。

6. 对比一下lazy委托和observable委托:lazy委托专注于getValue(),observable委托专注于setValue()

7. map委托

  • 特点:对于属性的访问,直接委托给一个map对象。
  • 要求:map的key要同属性名保持一致。
  • 对于val属性:

    class User(map: Map<String, Any?>) {
        val name: String by map
        val age: Int by map
    }
    
    
    fun main() {
    
        val user = User(mapOf(
                "name" to "David Lee",
                "age" to 25
        ))
    
        println(user.name)  //输出 David Lee
        println(user.age)   //输出 25
    
    }
    
  • 对于var属性:

    class Student(map: MutableMap<String, Any?>) {
    
        var name: String by map
        var age: Int by map
        var address: String by map
    }
    
    fun main(){
        val map: MutableMap<String, Any?> = mutableMapOf(
                    "name" to "Alice",
                    "age" to 23,
                    "address" to "beijing"
            )
    
        val student = Student(map)
        println(student.name)       //Alice
        println(student.age)        //23
        println(student.address)    //beijing
    
        println("---------------------------------")
    
        student.address = "hefei"
        println(student.address)    // hefei
        println(map["address"])     // hefei
    }
    

    对比可以得知:

    • val的map委托的对象是Map<String, Any?>,var的map委托的对象MutableMap<String, Any?>
    • 对于var属性,对于MutableMap中的value的修改,会同步到属性值;反之亦然。

8. 现在再回过头反编译一下我们自己动手实现的属性委托:

getStr的反编译结果就没贴了,同setStr类似。通过这俩截图,我们可以知道:kotlin编译器为RealSub类生成了两个重要的部分:

  • Delegate类型的实例成员:对于setStr()的实现逻辑,委托给Delegate类型的委托对象
  • static final的KProperty[] $$delegatedProperties: 在static代码块中初始化,存储被委托属性的KProperty,然后在后续的setValue、getValue的调用中作为其第二个参数。

9. 提供委托

终于写到最后一个部分了,有点兴奋也有点疲劳。前面我们介绍的属性委托,我们介入的环节都是对于属性的访问,实际上我们还可以对于委托对象的生成(或者说选取)进行介入:

class People {
    val name: String by DelegateProvider()
    val address: String by DelegateProvider()
}

class DelegateProvider {

    operator fun provideDelegate(thisRef: People, property: KProperty<*>): ReadOnlyProperty<People, String> {
        println("I'm in provideDelegate.")
        checkProperty()
        return RealDelegate()
    }

    private fun checkProperty() {
        val random = Random.Default
        if (!random.nextBoolean()) {
            throw RuntimeException("failed to create delegate.")
        }
    }
}

class RealDelegate : ReadOnlyProperty<People, String> {
     override fun getValue(thisRef: People, property: KProperty<*>): String {
         return "kotlin"
     }
}

先撇开中间的DelegateProvider类不看,其他两个类的实现符合我们之前介绍的理论。那么中间的这个类有什么特点或者说什么要求呢?必须提供一个provideDelegate的方法,同样地对于它的方法签名是有要求的:

operator fun provideDelegate(thisRef: T, property: KProperty<*>): RealOnlyProperty<T, R>

或者

operator fun provideDelegate(thisRef: T, property: KProperty<*>): ReadWriteProperty<T, R>

再回到代码实现中来,我们这里通过checkProperty方法来模拟相关逻辑检查,添加main方法进行测试:

fun main() {

    val people = People()
    println(people.name)
    println(people.address)
}

然后看人品随机,多运行几次吧,肯定有不抛异常的时候。篇幅有点长,谢谢耐心阅读!

  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值