您当前的位置:首页 > 计算机 > 编程开发 > Kotlin

kotlin中的委托

时间:02-07来源:作者:点击数:

理解委托

委托是一种设计模式,具体的操作不用自己实现,而是把操作委托给另一个辅助的对象,我们把这个辅助对象称为委托。

注:本篇文章内容来自《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)
    }

}

与之前的版本相比,这次代码做了一些更改:

  • 现在,按照约定的需要,getValue和setValue函数被标志了operator。
  • 这些函数加了两个参数:一个用于接收属性的实倒(Person),用来设置或读取属性,另一个用于表示属性本身(KProperty)。
  • 把name属性从主构造方法中删除了,因为现在可以通过KProperty访问属性名称了。

终于,你可以见识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中),以及在访问该属性时做点什么(比如添加验证、更改通知等)。所有这一切都可以用紧凑的代码完成。我们再来看看标准库中委托属性的另一个用法,然后看看如何在自己的框架中使用它们。

在map中保存属性值

委托属性发挥作用的另一种常见用法,是用在有动态定义的属性集的对象中。这样的对象有时被称为自订(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方法负债检索和更新数据库中的信息。

总结:

  • 委托属性可以用来重用逻辑,这些逻辑控制如何存储、初始化、访问和修改属性值,这是用来构建框架的一个强大的工具。
  • lazy标准库函数提供了一种实现惰性初始化属性的简单方法
  • Delegate.observable函数可以用来添加属性更改的观察者。
  • 委托属性可以使用任意map来作为属性委托,来灵活来处理具有可变属性集的对象。
方便获取更多学习、工作、生活信息请关注本站微信公众号城东书院 微信服务号城东书院 微信订阅号
推荐内容
相关内容
栏目更新
栏目热门