美文网首页kotlin入门潜修
kotlin入门潜修之类和对象篇—委托及其原理(一)

kotlin入门潜修之类和对象篇—委托及其原理(一)

作者: 寒潇2018 | 来源:发表于2018-12-24 16:23 被阅读0次

    本文收录于 kotlin入门潜修专题系列,欢迎学习交流。

    创作不易,如有转载,还请备注。

    委托

    提起委托就会想到代理,这两个名词真是傻傻分不清楚。代理模式是23种设计模式中非常常用的一种,大名鼎鼎的spring框架都大量使用了代理模式。所以,这个大家想必已经很熟悉了。那么委托又是什么?和代理有啥区别?

    有朋友可能会听到委托设计模式,发现23种设计模式当中并没有这个设计模式,因为设计模式的创作者认为委托属于一种编程技巧,更多的服务于其他设计模式,而不是单独作为一个模式而存在。当然还有很多人认为代理模式其实就是委托模式(因为他们实在太像了),反正区分来区分去,迷迷糊糊的。

    那么如果非要将代理和委托从设计上做个比较,有啥区别?从设计模式的角度来看,代理模式是为了屏蔽被代理者的实现细节,而委托则更多的是想像继承一样来实现最大化的资源复用。代理模式中代理者和被代理者必须要实现同一个接口,而委托则不需要。代理模式可以看做委托的一个特例。

    当然也可以从语义上来进行理解,毕竟命名一定程度上能体现含义。从调用方来看,委托具有被动性,更多的是被动的为调用方完成调用方想要完成的功能(可以理解为调用方自己不想干的事全交给委托者去干了);而代理则具有主动性,更多的是主动屏蔽被代理者的细节,然后完成调用方想要的功能。

    好了,关于二者的异同讨论就到此为止了,下面来看下kotlin中的委托。

    委托实现

    本节看一下委托的实现与调用。需要说明的是,本章节更多的是侧重于阐述类、方法的委托等,而非属性委托,属性委托会在下个章节阐述。

    在kotlin中,为委托提供了一个关键字:by。示例如下:

    interface IWork {//声明了一个接口IWork,有个doWork方法,用来完成工作
        fun doWork()
    }
    class Boss : IWork {//Boss类,老板,实现了自己的工作方法
        override fun doWork() {
            println("work done")
        }
    }
    //重点来了,定义了一个秘书类,这个类也继承了IWork接口,
    //但是,这个类并没有进行任何实现,而是委托给了要传入的work对象来实现
    //这里这个work对象在我们的测试例子中就是Boss对象
    class Secretary(work: IWork) : IWork by work
    //测试类,用于测试委托的调用
    class Main {
        companion object {
            @JvmStatic
            fun main(args: Array<String>) {
                Secretary(Boss()).doWork()//因为这里传入的是Boss对象,所以说上面的work实际上就是Boss对象。
            }
        }
    }
    

    上面代码清晰明了,这里主要说下上面代码要表达的意义:首先上面有两个角色,一个是秘书,一个是老板。然后他们都有自己的工作,但是现在秘书的工作无法自己完成,所以只能委托给老板来代为完成了。现实中我们肯定要自己告诉老板一声,而在kotlin中,我们只需要使用by关键字告知编译器我们要委托给其他人来帮我工作了,然后在实际触发工作的地方填入我们要委托的具体的对象就ok了。

    上面的白话文用kotlin语言来描述一遍:实现委托的关键点在于by关键字,by后面所跟的对象(本例子中就是work对象)就是帮我们实现功能的被委托对象。委托对象(本例子中就是secretary对象)须持有被委托对象的引用,这样才能使用被委托对象的功能。而所谓实际触发工作的地方就是main方法中的测试语句:Secretary(Boss()).doWork(),Secretary的入参Boss()对象就是要委托实现功能的对象。

    这里先明确一个概念:委托整个过程可以描述为,当前对象“无法完成”某项功能,而是委托给了其他对象来代为完成的过程。这个“当前对象”就称为委托对象或委托者,而“其他对象”则称为被委托对象或被委托者。在我们示例中,Secretary对象就是委托者,而Boss对象则是被委托者。

    从示例中还可以看出,Secretary和Boss都实现了同样的IWork接口,那么是不是要实现委托就必须要实现同一个接口呢?

    对于kotlin中的委托可以这么认为。首先,kotlin只允许委托实现接口类型(连抽象类都不行),因此对于委托者和被委托者来说必须要实现一个接口。其次,既然要完成委托功能,自然要知道委托者委托了什么,所以被委托者要实现和委托者一样的接口(这样二者都必须实现接口中的方法,被委托者也就知道了委托者要委托的功能了,这里所谓功能就是一个个方法)。

    再说下by关键字,by后面的对象就是被委托对象(本例中即是work对象),这个对象的类型就是委托对象(Secretary)要实现的接口类型。实际上,在委托对象的内部会存储这个被委托对象,并且kotlin编译器会为委托对象生成接口IWork中的所有方法。

    接下来看看使用委托来复写接口方法成员的例子。

    interface IWork {
        fun doWork()
        fun doWorkNow()//注意这里,我们增加了一个doWorkNow的方法
    }
    //被委托者Boss类实现了IWork接口
    class Boss(val desc: String) : IWork {
        override fun doWorkNow() {
            println(desc)
        }
        override fun doWork() {
            println(desc)
        }
    }
    //委托者Secretary的委托实现
    class Secretary(work: IWork) : IWork by work{
        override fun doWorkNow() {//注意这里复写了doWorkNow方法
            println("do work just now")
        }
    }
    //测试类
    class Main {
        companion object {
            @JvmStatic
            fun main(args: Array<String>) {
                val boss = Boss("do work no matter what time");
                Secretary(boss).doWork()//打印'do work no matter what time '
                Secretary(boss).doWorkNow()//打印'do work just now',这是因为Secretary复写了doWorkNow方法
            }
        }
    }
    

    从打印日志可以看出,当委托者(Secretary)实现接口中的方法时,会优先调用其实现的方法,而不再调用被委托者中(此处是Boss)的方法。

    但是当委托者和被委托者都复写了接口属性时,委托者复写的属性无法覆盖被委托者中的属性,换句话说就是被委托者中的方法无法访问到委托者中的属性。示例如下:

    interface IWork {
        val workTime: String//注意这里增加了一个属性workTime,表示工作时间
        fun doWork()
    }
    
    class Boss(time: String) : IWork {
       //Boss复写了workTime,表明了自己的工作时间
        override val workTime = "boss work time : $time"
        override fun doWork() {
            println(workTime)//打印工作时间
        }
    }
    //委托实现类Secretary
    class Secretary(work: IWork) : IWork by work {
       //同样复写了workTime,表明了自己的工作时间
        override val workTime: String = "secretary work time: tomorrow"
    }
    
    class Main {
        companion object {
            @JvmStatic
            fun main(args: Array<String>) {
                val boss = Boss("today")
                val secretary = Secretary(boss)
                secretary.doWork()//'打印boss work time : today'
                println(secretary.workTime)//打印'secretary work time: tomorrow'
            }
        }
    }
    

    从上面代码执行后的打印日志可知,委托者(secretary)复写了workTime:override val workTime: String = "secretary work time: tomorrow",被委托者(boss)也复写了workTime:override val workTime = "boss work time : $time",当我们通过委托机制打印workTime的时候(对应于main方法中的secretary.doWork()语句),发现打印的实际上是被委托者(boss类)中的workTime值,而不是委托者中的workTime值。为了对比,我们还刻意在代码的最后一行打印出了委托者(secretary)中的workTime值,发现该值才是委托者自己复写的workTime,更加证明了被委托者无法访问委托者中的属性成员的论证。

    委托属性

    对于委托属性,显而易见的好处就是不用像继承那样对每份属性都实现一份。很多场景下,我们只需要实现一次即可。比如:

    1. 延迟属性(lazy properties)。这类属性只会在第一次访问的时候进行值计算。
    2. 可观察属性(observable properties)。这类属性通常会被其他观察者进行监听,并在属性变更的时候发送通知给这些观察者。

    等等...
    所以,kotlin为我们提供了代理属性。是实现关键字依然是by,示例如下:

    //被委托者类Delegate,这里将会接收委托者的属性委托请求
    class Delegate {
    //getValue操作符
        operator fun getValue(thisRef: Any?, property: KProperty<*>): String = "thisRef = $thisRef; property = $property"
    //setValue操作符
        operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
            println("thisRes = $thisRef; property = $property; value = $value")
        }
    }
    //委托者类Example,这里将会将其属性计算委托给Delegate实现
    class Example {
    //属性委托的语法,同上一个章节阐述的一样,使用by关键字修饰即可。
        var test: String by Delegate()
    }
    //测试类
    class Main {
        companion object {
            @JvmStatic
            fun main(args: Array<String>) {
                val example = Example()//生成一个委托者对象
                println(example.test)//这里读取了委托者对象的test属性值
                example.test = "new value"//这里是写操作,改变了委托者对象的test属性值
            }
        }
    }
    

    为了更清晰的认知属性委托机制,上面代码刻意在两个关键处打印了日志,一个是读的时候,一个是写的时候。打印结果如下:

    thisRef = Example@5910e440; property = var Example.test: kotlin.String
    thisRes = Example@5910e440; property = var Example.test: kotlin.String; value = new value
    

    结合打印结果以及kotlin对属性委托的语法定义,我们可以对属性委托总结以下几点:

    1. 属性委托的语法结构为:
    val/var <property name>: <Type> by <expression>
    

    by前面的语法和普通定义变量的语法没什么区别,主要是by关键字以及其后面的expression。by是kotlin规定的实现委托的关键字,没什么好说的。而expression怎么理解?其实这里的expression表达的就是被委托者,这是因为委托者属性中的get和set方法都会给代理到被委托者所提供的getValue和setValue方法上。

    1. 由代码实现以及第一条最后的分析可知,被委托者(即属性委托的实现者)必须要实现getValue方法(对于var属性还必须要实现setValue方法),但是不需要实现任何接口。且对于getValue方法其返回值必须和委托者中的属性类型保持一致。
    2. 从打印日志可知,被委托者会持有委托者的引用(即thisRef)以及所要代理的属性声明(注意是属性的声明,而不是属性的值)。当对属性进行写操作时(即调用setValue)还会传入新值。
    3. 从kotlin1.1之后,我们可以在方法体中以及代码块中定义委托属性,比如针对上个例子,我们可以添加个新方法,在新方法中定义委托属性:
    fun m1() {
        var test: String by Delegate()//在方法体中定义委托属性
        if (true) {//仅仅为了测试,所以条件里面写了true
            var test: String by Delegate()//在代码块中定义委托属性
        }
    }
    
    

    kotlin提供的几个标准委托

    kotlin标准库已经为我们提供了几类不同场景下的委托(对应于前面提到的几种属性委托的场景)。本章节来一一看一下。

    Lazy

    kotlin中为延迟属性实现了一个标准库方法lazy,该方法接收一个lambda表达式并且返回一个Lazy<T>类型的实例,该实例就是被委托者。当属性第一次被访问的时候,lazy会调用get方法进行属性计算,之后就会缓存这个值,然后在后面调用的时候直接返回缓存的值。

    延迟属性示例如下:

                val lazyValue: String by lazy {
                    println("first call...")
                    "hello word"
                }
    //这里刻意打印两次,来看看是否会多次调用属性的get方法
                println(lazyValue)
                println(lazyValue)
    

    执行结果打印如下:

    first call...
    hello word
    hello word
    

    从打印结果可知,lazy属性确实只会在第一次用到的时候进行get计算并将计算结果进行缓存,之后再取值的时候就直接取缓存值。

    默认情况下,lazy属性是线程安全的,所以在多个线程访问的情况下,该属性只会缓存第一个线程的执行结果。

    如果我们不需要在初始化时保证属性值的同步,可以传递参数LazyThreadSafetyMode.PUBLICATION 给lazy方法,这样就可能会出现多个线程同时访问该属性的状况,换句话说,lazy属性的初始化值将是未知的,可能由任意一个线程进行初始化。

    如果我们确定属性的运行环境只有一个单线程,那么可以传递参数LazyThreadSafetyMode.NONE给lazy方法,但这种模式如果工作在多线程中,将不会保证多线程情况下是否会产生死锁。

    Observable

    在kotlin中,为可观察者属性提供了一个observable()方法,该方法接收两个参数,一个是初始值,一个是修改处理器(handler)。handler会在每次赋值的时候调用,handler接收三个参数:当前的属性声明、属性旧值、属性新值。示例如下:

    //这里定义了一个可观察者属性name
                var name: String by Delegates.observable("zhang san") { property, oldValue, newValue ->
                    println("$property value changed: $oldValue -> $newValue")
                }
    //通过改变name的值来观察可观察属性的执行状况
                name = "lisi"
                name = "wangwu"
                name = "songliu"
    

    上面代码执行完成后,打印结果如下:

    var name: kotlin.String value changed: zhang san -> lisi
    var name: kotlin.String value changed: lisi -> wangwu
    var name: kotlin.String value changed: wangwu -> songliu
    

    由打印日志可知,可观察属性可以接受一个初始化值,当值改变的时候就会回调修改处理器handler。

    kotlin允许对可观察属性进行赋值拦截,如下所示:

                var name2:String by Delegates.vetoable("zhang san"){
                    property, oldValue, newValue ->
                    println("$property value changed: $oldValue -> $newValue")
                    true//注意这里返回了true
                }
                name2 = "lisi"
                println(name2)
    
    
                var name3:String by Delegates.vetoable("zhang san"){
                    property, oldValue, newValue ->
                    println("$property value changed: $oldValue -> $newValue")
                    false//这里返回了false
                }
                name3 = "lisi"
                println(name3)
    

    上面代码中,定义了两个可观察者属性name2、name3,但是它们是通过Delegates.vetoable方法来实现委托的,两个属性唯一的区别就是最后的返回值不同,name2返回了true,而name3返回了false。其执行结果如下所示:

    var name2: kotlin.String value changed: zhang san -> lisi
    lisi
    var name3: kotlin.String value changed: zhang san -> lisi
    zhang san
    

    从打印结果发现,name3属性值竟然没有被改变!其实,这就是Delegates.vetoable的作用,可以在新值赋值之前拦截该操作,进而无法改变该属性值。实现这种效果只需要简单的将其修改处理器返回值返回false即可。

    Map

    在kotlin中,map也可以作为属性委托的被委托者出现,主要是用于属性值的缓存。如下所示:

    //Person类,这里我们将其属性委托给map来进行保存
    class Person(val map: Map<String, Any?>) {
        val name: String by map//委托给map保存
        val age: String by map
    }
    //生成person对象
    val person = Person(mapOf(
            "name" to "zhangsan",
            "age" to "30"
    ))
    //测试方法,打印person的属性值
    fun test() {
        println(person.name)//打印'zhangsan'
        println(person.age)//打印'30'
    }
    

    provideDelegate操作符

    我们可以通过provideDelegate操作符来提供一个自己的委托,这样定义的委托可以同标准库提供的lazy、map等使用方法一致,同时可以在保证委托属性创建的同时加入我们自己的执行逻辑。

    假如现在有个需求需要实现一个ui界面,这个UI界面涉及到很多资源,而每个资源都会绑定一个唯一的id,现在我们的目标是需要实现一个资源绑定的功能(bindResource),该功能可以通过id完成资源的初始化,更重要的是,在这个资源初始化的过程之前还能完成对属性的校验。这个属性校验包括属性的可见性、属性名、属性类型等等。示例如下:

    //资源的委托实现,这里仅仅是返回了一条字符串说明
    class ResourceDelegate : ReadOnlyProperty<UI, String> {
        override fun getValue(thisRef: UI, property: KProperty<*>): String {
            return "$thisRef delegate ${property.name}"
        }
    }
    //资源加载类,用于加载资源并校验属性
    class ResourceLoader(val id: Int) {
            private fun checkProperty(thisRef: UI, name: String) {
    //在这个方法里面可以完成对属性名称的校验
            println("$thisRef $name check done...")
        }
    //类ResourceLoader必须要定义下面这个方法,而且不仅方法声明要和下面一致,名字也必须是provideDelegate
        operator fun provideDelegate(
                thisRef: UI,
                prop: KProperty<*>): ReadOnlyProperty<UI, String> {        
            checkProperty(thisRef, prop.name)
            return ResourceDelegate()
        }
    }
    //ui类,写ui的时候经常需要用到资源绑定功能,bindResource就是根据id完成资源的初始化,并对属性进行校验
    class UI {
    //提供一个名为bindResource的方法,这个方法返回值是ResourceLoader
    //当我们通过by关键字委托给该方法实现功能时,会自动调用
    //ResourceLoader中的provideDelegate方法
        private fun bindResource(id: Int): ResourceLoader {
            return ResourceLoader(id)
        }
        val image by bindResource(10000)//使用方法同使用标准库中的lazy等委托一样
        val text by bindResource(20000)
    }
    

    上面代码提供了一个我们自己定义的委托,这个委托并不是简单的委托方法实现,而是能够让我们像标准库提供的委托一样使用。代码中的注释已经比较详尽,需要注意的是提供委托的类必须要实现operator fun provideDelegate方法。

    当然,我们也可以不使用provideDelegate来完成属性name的检测,但是这个显然不是很方便。比如我们采用扩展方法来实现上述功能:

    fun UI.bindResource(id: Int, ui: UI,prop: KProperty<*>): ReadOnlyProperty<UI, String> {
        checkProperty(id, ui, prop)
        return ResourceDelegate()
    }
    

    这里我们演示了不通过委托来实现资源绑定以及属性校验的功能,但是这里的缺点是显而易见的:我们必须要人为的传入属性所在的对象以及要校验的属性。而这也正是通过委托实现的优点。

    下篇文章将会阐述委托的实现原理。

    相关文章

      网友评论

        本文标题:kotlin入门潜修之类和对象篇—委托及其原理(一)

        本文链接:https://www.haomeiwen.com/subject/neopkqtx.html