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

本文收录于 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()
}

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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,053评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,527评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,779评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,685评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,699评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,609评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,989评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,654评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,890评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,634评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,716评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,394评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,976评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,950评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,191评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,849评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,458评论 2 342

推荐阅读更多精彩内容