委托是一种设计模式,具体的操作不用自己实现,而是把操作委托给另一个辅助的对象,我们把这个辅助对象称为委托。
注:本篇文章内容来自《Kotlin实战》一书经过自己的消化与学习整理的。
类委托,委托和代理似乎是在讲同一个东西,比如,一个老板想喝咖啡,老板嘛,比较懒,不想自己去买,于是他叫经理给他买,经理又叫他的一个手下去买。在这个关系中,经理可以理解为一个代理,代老板买东西,但是经理也没有直接去买,而是把工作交给了他的手下去买,这就可以理解成委托,经理(代理)委托他的手下去买咖啡。
我们可以这样理解,经理是一个代理,他可以做很多的事情,但是真正做事情的时候是委托给他的手去做的。
在代码中写一个示例,我们写一个集合的代理类,这个代理可以完成集合的各种操作,但是它是委托给ArrayList来完成的,如下:
class DelegatingCollection<T> : Collection<T> {
private val innerList = ArrayList<T>()
override val size: Int get() = innerList.size
override fun contains(element: T): Boolean = innerList.contains(element)
override fun containsAll(elements: Collection<T>): Boolean = innerList.containsAll(elements)
override fun isEmpty(): Boolean = innerList.isEmpty()
override fun iterator(): Iterator<T> = innerList.iterator()
}
如上代码,DelegatingCollection是一个集合代理类,它的功能和ArrayList一模一样,因为它的功能都是委托给ArrayList来完成的,那我们为什么不直接使用ArrayList而要使用代理类呢?因为使用代理类我们是可以做一些其他事情的,比如在代理中打印函数的执行时间、调用次数或者是检查参数合法性等等。
写一个代理类还是比较容易的,但是如果方法很多,这样写起来也是有点烦人的,通过Kotlin的类委托就可以解决这个烦恼,如下:
class DelegatingCollection<T>(innerList: Collection<T> = ArrayList<T>()) : Collection<T> by innerList
Ok,就是这么清爽,这就像Kotlin中的data class一样,自动为你生成需要的方法。
我们也可以覆盖掉某些方法,自己实现,如下:
class DelegatingCollection<T>(val innerList: Collection<T> = ArrayList<T>()) : Collection<T> by innerList {
private var invokeCount = 0
override fun isEmpty(): Boolean {
invokeCount++
return innerList.isEmpty()
}
}
如上代码,我们覆盖了isEmpty()函数,在其中计算该函数被调用的次数,虽然最后也是委拖给innerList来完成isEmpty的判断,但是在其他场合,你完全可以使用自己的实现而不进行委托,比如经理,他的很多工作是委托给他的手下去完成的,但是有一些工作他不想委托给他的手,那他就自己实现了罗,比如泡妞自己实现就比较好_。
需要注意的是,和Java的动态代理一样,需要继承一个接口才行,比如下面的写法编译就报错了:
class MyList<T>(list: List<T> = listOf()) by list
因为这个MyList类没有继承接口,则无法知道把什么方法委托给list对象。而且要继承的必须是一个接口,不能是一个类,如下代码也是会编译就报错的:
open class Parent {
open fun sayHello() {
println("Hello Parent")
}
}
class ChildA : Parent() {
override fun sayHello() {
println("Hello ChildA")
}
}
class ChildB(child: ChildA = ChildA()) : Parent by child // 此处编译报错
如上代码,编译报错信息为:Only interfaces can be delegated to,意思为:只有接口可以被委托,Parent是一个类,不能被委托,所以我们把Parent修改为接口即可被委托了,如下:
interface Parent {
fun sayHello()
}
其实也比较容易理解,Parent by child 意思就是把Parent中的方法调用委托给child对象,那然后你把方法的实现委托给child了,那你自己就不应该有实现了,应该是个接口。而且child也必须是Parent接口类型的对象,也就是说类型是Parent也可以,是Parent的子类也可以,如下两个写法都是OK的:
class ChildB(child: ChildA = ChildA()) : Parent by child
class ChildC(child: Parent = ChildA()) : Parent by child
总结就是:只能把接口进行委托,且只能委托给这个接口的实例对象。换句话说,如果你想把一些工作委托给一个对象处理,则这个对象必须是实现了某个接口的,否则的话就无法进行委托。
我们发现前面的示例代码实现了Collection接口,但是该接口没有add方法呀,我们知道Collection接口有很多分类,我们可以使用它的子接口List接口,对应到Kotlin中的话应该使用MutableList,因为Mutable类型的List才有add方法,默认的List是不可改List,只能读,是没有add方法的,示例如下:
class MyList<T>(list: MutableList<T> = mutableListOf()) : MutableList<T> by list
fun main() {
val myList = MyList(mutableListOf<String>())
myList.add("Hello")
myList.add("World")
}
属性委托是Kotlin中最独特和最强大的功能之一。
属性委托是把一个属性的访问器(即setter和getter)的逻辑委托给一个辅助对象。示例如下:
class Foo {
var p: Type
}
如上代码,这是一个简单的kotlin类,有一个属性p,我们以可以自定义该属性的setter和getter,如下:
class Foo {
var p: Type
set(value: Type) { ... }
get() = ...
}
如上代码完成了setter和getter的自定义,这些都是kotlin基础就有讲的。然后,我们可以把setter和getter的工作交给一个代理类来完成,如下:
class Foo {
private val delegate = Delegate()
var p: Type
set(value: Type) = delegate.setValue(..., value)
get() = delegate.getValue(...)
}
如上代码,调用属性p的setter和getter的具体实现逻辑交给delegate的setVaule和getValue来完成(setVaule仅适用于可变属性),setValue和getValue可以是成员函数,也可以是扩展函数。Delegate的简单实现如下:
class Delegate {
operator fun getValue(...) { ... }
operator fun setValue(..., value: Type) { ... }
}
按照Kotlin的约定,属性的委托类必须要有getValue和setValue方法(setVaule仅适用于可变属性),且必须使用operator修饰符进行修饰,方法的具体参数后面会讲解。只要符合这个约定,我们就可以使用Kotlin的简洁语法完成属性委托,如下:
class Foo {
var p: Type by Delegate
}
fun main() {
val foo = Foo()
val oldValue = foo.p // 调用的是delegate.getValue(...)
foo.p = newValue // 调用的是delegate.setValue(..., newValue)
}
一个真实的示例代码如下:
class Delegate {
private var value: Int = -1
operator fun getValue(person: Person, prop: KProperty<*>): Int {
return value
}
operator fun setValue(person: Person, prop: KProperty<*>, value: Int) {
this.value = value + 1
}
}
class Person {
var age: Int by Delegate()
}
fun main() {
val p = Person()
println("age = ${p.age}")
p.age = 49
println("age = ${p.age}")
}
打印结果如下:
age = -1
age = 50
惰性初始化是一种常见的模式,比如在设计单例的时候可以设计为懒加载的单例,即单例的初始化是惰性初始化的,用到的时候才把单例实例化,没用到的时候为null。
示例:一个Person有emails属性,保存了此人的所有邮件,当我们不访问这个属性的时候,它为null,当我们访问它的时候,如果为null就进行加载emails的操作(耗时操作),加载完成后emails就不为空了,当再次访问emails属性时,它就不会再执行加载emails的操作了,因为之前已经加载好了,这就是惰性初始化,不需要的时候为null,需要的时候才初始化,而且只初始化一次。示例代码如下:
class Person(val name: String) {
private var _emails: List<Email>? = null
val emails: List<Email>
get() {
if (_emails == null) {
_emails = loadEmails(this)
}
return _emails!!
}
}
这里使用了所谓的支持属性技术:即一个属性,用两个字段来完成,一个是_emails,用来存储这个属性的值,另一个是emails,用来提供对属性的读取。你需要两个属性来完成,因为他们具有不同的类型:_emails可以为空,而emails为非空,这种技术经常会使用到,值得熟练掌握。
但这个代码有点繁瑣,如果需要多个这样的惰性属性,那这个类就很臃肿了。而且,它并不总是正常运行:这个实现不是线程安全的,Kotlin提供了更好的解决方案,使用委托属性会让代码变得简单得多,通过lazy函数来返回委托的属性,lazy是一个标准的库函数,示例如下:
class Person(val name: String) {
val emails by lazy{ loadEmails(this) }
}
lazy函数返回一个对象,该对象具有一个名为getValue且签名正确的方法,因此可以把它与by关键字一起使用来创建一个委托属性。lazy函数的参数是一个lambda,可以调用它来初始化这个值。默认情况下,lazy函数是线程安全的,如果需要,可以设置其他选项来告诉它要使用哪个锁,或者完全避开同步,如果该类永远不会在多线程环境中使用。lazy函数返回的Lazy实现类是SynchronizedLazyImpl,如下:
private class SynchronizedLazyImpl<out T>(initializer: () -> T, lock: Any? = null) : Lazy<T>, Serializable {
private var initializer: (() -> T)? = initializer
private var _value: Any? = null
private val lock = lock ?: this
override val value: T
get() {
if (_value != null) {
return _value as T
}
return synchronized(lock) {
if (_value != null) {
(_value as T)
} else {
_value = initializer!!()
(_value as T)
}
}
}
}
如上代码,我进行了一些简化与修改,以使其更容易理解,我们的loadEmails()函数其实就是一段lambda参数,会传给SynchronizedLazyImpl的initializer属性进行保存,当我们调用Person的emails属性时,其实调用的是getEmails()函数,而这个函数调用的是SynchronizedLazyImpl的getValue()函数,这个函数就会判断需要的值是否已经初始化,如果初始化了就直接返回,否则就进行初始化,而且这个操作是线程安全的。 原理还是比较简单的,就是把emails属性的getter委托给Lazy的getValue()函数,让我奇怪的是,这个getValue函数并没有符合Kotlin委托属性的约定啊,按道理getValue函数的应该大概是这样的:
fun getValue(person: Person, prop: KProperty<*>): List<Email> {
。。。
}
到这里我们可以了解到by和by lazy的区别了,by用于属性委托,需要我们自己创建委托类,这个委托类必须要有getValue函数,如果是可写属性的话还需要有setValue函数,且方法签名要符合Kotlin的约定,如果这个属性是只读的,则我们可以使用lazy来帮我们创建一个委托类。这就是by 和by lazy的区别了,我们自己创建委托类也能实现属性的延迟加载,但是如果自己写,则每次都要写一堆模板代码,所以使用lazy来创建委托类代码就比较简洁了。
通过另一个例子来了解属性委托的应用:当一个对象的属性更改时通知监听器,在这许多不同的情况下都很有用,例如:当对象显示在UI时,你希望在对象变化时UI能自动刷新。Java具有用于此类通知的标准机制:PropertyChangeSupport和PropertyChangeEvent类。让我们看看在Kotlin中不使用委托属性的情况下,该如何使用它们,然后我们再将代码重构为用委托属性的方式。
PropertyChangeSupport类维护了一个监听器列表,并向它们发送PropertyChangeEvent事件。要使用它,你通常需要把这个类的一个实例存储为bean类的一个字段,并将属性更改的处理委托给它。在Android的类文档中有这样一个示例:
public class MyBean {
private final PropertyChangeSupport pcs = new PropertyChangeSupport(this);
public void addPropertyChangeListener(PropertyChangeListener listener) {
this.pcs.addPropertyChangeListener(listener);
}
public void removePropertyChangeListener(PropertyChangeListener listener) {
this.pcs.removePropertyChangeListener(listener);
}
private String value;
public String getValue() {
return this.value;
}
public void setValue(String newValue) {
String oldValue = this.value;
this.value = newValue;
this.pcs.firePropertyChange("value", oldValue, newValue);
}
[...]
}
如果我们要很多个JavaBean,而且这些JavaBean的属性发生改变时,都想得到监听,为了避免要在每个JavaBean类中去添加一段重复代码,可以先创建一个基类,如下:
open class BaseBean {
protected val pcs = PropertyChangeSupport(this)
fun addPropertyChangeListener(listener: PropertyChangeListener) {
pcs.addPropertyChangeListener(listener)
}
fun removePropertyChangeListener(listener: PropertyChangeListener) {
pcs.removePropertyChangeListener(listener)
}
}
现在我们来写一个Person类,定义一个只读属性(作为一个人的名称,一般不会随时更改)和两个可写属性:年龄和工资,当这个人的年龄或工资发生变化 时,这个类将通知它的监听器。代码如下:
class Person(val name: String, age: Int, salary: Int) : BaseBean() {
var age: Int = age
set(newValue) {
val oldValue = field
field = newValue
pcs.firePropertyChange("age", oldValue, newValue)
}
var salary: Int = salary
set(newValue) {
val oldValue = field
field = newValue
pcs.firePropertyChange("salary", oldValue, newValue)
}
}
fun main() {
val p = Person("Dmitry", 34, 2000)
p.addPropertyChangeListener { event ->
println("Property ${event.propertyName} changed from ${event.oldValue} to ${event.newValue}")
}
p.age = 35 // 输出:Property age changed from 34 to 35
p.salary = 2100 // 输出:Property salary changed from 2000 to 2100
}
setter中有很多重复的代码,我们把它提取到一个类,如下:
class ObservableProperty(
val propName: String,
var propValue: Int,
val changeSupport: PropertyChangeSupport
) {
fun getValue(): Int = propValue
fun setValue(newValue: Int) {
val oldValue = propValue
propValue = newValue
changeSupport.firePropertyChange(propName, oldValue, newValue)
}
}
class Person(val name: String, age: Int, salary: Int) : PropertyChangeAware() {
val _age = ObservableProperty("age", age, changeSupport)
var age: Int
get() = _age.getValue()
set(newValue) = _age.setValue(newValue)
val _salary = ObservableProperty("salary", age, changeSupport)
var salary: Int
get() = _salary.getValue()
set(newValue) = _salary.setValue(newValue)
}
fun main() {
val p = Person("Dmitry", 34, 2000)
p.addPropertyChangeListener(
PropertyChangeListener { event ->
println("Property ${event.propertyName} changed from ${event.oldValue} to ${event.newValue}")
}
)
p.age = 35 // 输出:Property age changed from 34 to 35
p.salary = 2100 // 输出:Property salary changed from 2000 to 2100
}
上面的示例代码不就是属性委托吗?如果能使用Kotlin的语法糖来完成就好了,但是在此之前,你需要更改ObservableProperty方法的签名,以符合Kotlin的约定方法。
class ObservableProperty(
var propValue: Int,
val changeSupport: PropertyChangeSupport
) {
operator fun getValue(p: Person, prop: KProperty<*>): Int = propValue
operator fun setValue(p: Person, prop: KProperty<*>, newValue: Int) {
val oldValue = propValue
propValue = newValue
changeSupport.firePropertyChange(prop.name, oldValue, newValue)
}
}
与之前的版本相比,这次代码做了一些更改:
终于,你可以见识Kotlin委托属性的神奇了,如下:
class Person(val name: String, age: Int, salary: Int) : BaseBean() {
var age: Int by ObservableProperty(age, pcs)
var salary: Int by ObservableProperty(salary, pcs)
}
你不用手动去实现可观察的属性逻辑,可以使用Kotlin标准库,它已经包含了类似于ObservableProperty的类。标准库和这里使用的PropertyChangeSupport类没有耦合,因此你需要传递一个lambda,来告诉它如何通知属性值的更改。可以这样做:
class Person(val name: String, age: Int, salary: Int) : BaseBean() {
private val observer = { prop: KProperty<*>, oldValue: Int, newValue: Int ->
pcs.firePropertyChange(prop.name, oldValue, newValue)
}
var age: Int by Delegates.observable(age, observer)
var salary: Int by Delegates.observable(salary, observer)
}
如果是这样的话,感觉BaseBean都不需要了,如下:
class Person(val name: String, age: Int, salary: Int, val onChange: ((property: KProperty<*>, oldValue: Int, newValue: Int) -> Unit)) : BaseBean() {
var age: Int by Delegates.observable(age, onChange)
var salary: Int by Delegates.observable(salary, onChange)
}
by右边的表达式不一定是构造函数直接创建对象,也可以是函数调用、另一个属性或任何其他表达式,只要这个表达式的值能够被编译器用正确的参数类型来调用getValue和setValue的对象。与其他约定一样,getValue和setValue可以是对象自己声明的方法或扩展函数。
让我们来总结一下委托属性是怎么工作的,假设你已经有了一个具有委托属性的类:
class C {
var prop: Type by MyDelegate()
}
val c = C()
MyDelegate实例会被保存到一个隐藏的属性中,它被称为:<delegate>,编译器也将用一个KProperty类型的对象来代表这个属性,它被称为:<property>。编译器生成的代码如下:
class C {
private val <delegate> = MyDelegate()
var prop: Type
get() = <delegate>.getValue(this, <property>)
set(value: Type) = <delegate>.setValue(this, <property>, value)
}
因为,在每个属性访问器中,编译器都会生成对应的getValue和setValue方法,如下:
val c = C()
val x = c.prop // 编译为: val x = <delegate>.getValue(c, <property>)
c.prop = x // 编译为: <delegate>.setValue(c, <property>, x)
这个机制非常简单,但它可以实现许多有趣的场景。你可以自定义存储该属性值的位置(map、数据库表或者用户会话的Cookie中),以及在访问该属性时做点什么(比如添加验证、更改通知等)。所有这一切都可以用紧凑的代码完成。我们再来看看标准库中委托属性的另一个用法,然后看看如何在自己的框架中使用它们。
委托属性发挥作用的另一种常见用法,是用在有动态定义的属性集的对象中。这样的对象有时被称为自订(expando)对象。例如,考虑一个联系人管理系统,可以用来存储有关联系人的任意信息。系统 中的每个人都有一些属性需要特殊处理(例如名字),以及每个人特有的数量任意的额外属性(例如,最小的孩子的生日)。
实现这种系统的一种方法是将人的所有属性存储在map中,不确定提供属性,来访问需要特殊处理的信息。来看个例子:
class Person {
private val _attributes = hashMapOf<String, String>()
fun setAttribute(attrName: String, value: String) {
_attributes[attrName] = value
}
val name: String
get() = _attributes["name"]!!
}
fun main() {
val p = Person()
val data = mapOf("name" to "Dmitry", "company" to "JetBrains")
for ((attrName, value) in data) {
p.setAttribute(attrName, value)
}
println(p.name) // 输出:Dmitry
}
这里使用了一个通用的API来把数据加载到对象中(在实际项目中,可以是JSON反序列化或类似的方法),然后使用特定的API来访问一个属性的值。把它改为委托属性非常简单,可以直接将map放在by关键字后面,如下:
class Person {
private val _attributes = hashMapOf<String, String>()
fun setAttribute(attrName: String, value: String) {
_attributes[attrName] = value
}
val name: String by _attributes
}
因为标准库已经在标准Map和MutableMap接口上定义了getValue和setValue扩展函数(定义在kotlin.collections.MapAccessors.kt文件中),所以这里可以直接这样用。属性的名称将自动用作在map中的键,属性值作为map中的值。
更改存储和修改属性的方式对框架的开发人员非常有用。假设数据库中Users的表包含两列数据:字符串类型的name和整形的age。可以在Kotlin中定义Users和User类。在Kotlin代码中,所有存储在数据库中的用户实体的加载和更改都可以通过User类的实例来操作。
object Users : IdTable() {
val name = varchar("name", length = 50).index()
val age = integer("age")
}
class User(id: EntityID) : Entity(id) {
var name: String by Users.name
var age: Int by Users.age
}
Users对象描述数据库表的一个表:它被声明为一个对象,因为它对应整个表,所以只需要一个实例。对象的属性表示数据表的列。
User类的基类Entity,包含了实体的数据库列与值的映射。特定User的属性拥有这个用户在数据库中指定的值name和age。
框架用起来会特别方便,因为访问属性会自动从Entity类的映射中检索相应的值,而修改过的对象会被标记成脏数据,在需要时可将其保存到数据库中。可以在Kotlin代码中编写user.age += 1,数据库中的相应实体将自动更新。
现在你已经充分了解了如何实现具有这种API的框架。每个实体属性(name, age)都实现为委托属性,使用对象(Users.name, Users.age)作为委托:
class User(id: EntityID) : Entity(id) {
var name: String by Users.name
var age: Int by Users.age
}
让我们来看看怎样显式地指定列的类型:
object Users : IdTable() {
val name: Column<String> = varchar("name", length = 50).index()
val age: Column<Int> = integer("age")
}
至于Column类,框架已经定义了getValue和setValue方法,满足Kotlin的委托约定:
operator fun <T> Column<T>.getValue(o: Entity, desc: KProperty<*>): T {
// 从数据库中获取值
}
operator fun <T> Column<T>.setValue(o: Entity, desc: KProperty<*>, value: T): T {
// 更新值到数据库
}
可以使用Column属性(Users.name)作为被委托属性(name)的委托。当在代码中写入user.age += 1时,代码的执行将类似于user.ageDelegate.setValue(user.ageDelegate.getValue() + 1) (省略了属性和对象实例的参数)的操作。getValue和setValue方法负债检索和更新数据库中的信息。
总结: