本文收录于 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,更加证明了被委托者无法访问委托者中的属性成员的论证。
委托属性
对于委托属性,显而易见的好处就是不用像继承那样对每份属性都实现一份。很多场景下,我们只需要实现一次即可。比如:
- 延迟属性(lazy properties)。这类属性只会在第一次访问的时候进行值计算。
- 可观察属性(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对属性委托的语法定义,我们可以对属性委托总结以下几点:
- 属性委托的语法结构为:
val/var <property name>: <Type> by <expression>
by前面的语法和普通定义变量的语法没什么区别,主要是by关键字以及其后面的expression。by是kotlin规定的实现委托的关键字,没什么好说的。而expression怎么理解?其实这里的expression表达的就是被委托者,这是因为委托者属性中的get和set方法都会给代理到被委托者所提供的getValue和setValue方法上。
- 由代码实现以及第一条最后的分析可知,被委托者(即属性委托的实现者)必须要实现getValue方法(对于var属性还必须要实现setValue方法),但是不需要实现任何接口。且对于getValue方法其返回值必须和委托者中的属性类型保持一致。
- 从打印日志可知,被委托者会持有委托者的引用(即thisRef)以及所要代理的属性声明(注意是属性的声明,而不是属性的值)。当对属性进行写操作时(即调用setValue)还会传入新值。
- 从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()
}
这里我们演示了不通过委托来实现资源绑定以及属性校验的功能,但是这里的缺点是显而易见的:我们必须要人为的传入属性所在的对象以及要校验的属性。而这也正是通过委托实现的优点。
下篇文章将会阐述委托的实现原理。