Kotlin:类委托和委托属性

我不是罗大锤 2021年11月23日 202次浏览

委托是一种设计模式,它的基本理念是:操作对象自己不会去处理某段逻辑,而是会把工作委托给另外一个辅助对象去处理。

一、类委托

类委托的核心思想在于将一个类的具体实现委托给另一个类去完成。在前面的章节中,我们曾经使用过Set这种数据结构,它和List有点类似,只是它所存储的数据是无序的,并且不能存储重复的数据。Set是一个接口,如果要使用它的话,需要使用它的具体的实现类,比如HashSet。而借助于委托模式,我们可以轻松实现一个自己的实现类。比如定义一个MySet,并让它实现Set接口,代码如下所示:

class MySet<T>(val helperSet: HashSet<T>) : Set<T> {
    override val size: Int
        get() = helperSet.size
    override fun contains(element: T) = helperSet.contains(element)
    override fun containsAll(elements: Collection<T>) = 
            helperSet.containsAll(elements)
    override fun isEmpty() = helperSet.isEmpty()
    override fun iterator() = helperSet.iterator()
}

可以看到,MySet构造函数中接收了一个HashSet参数,相当于一个辅助对象,然后在Set接口所有的方法实现中,我们都没有进行自己的实现,而是调用了辅助对象中相应的方法实现,这其实就是一种委托模式。

我们如果让大部分的方法实现调用辅助对象中的方法,少量部分的方法实现由自己来重写,甚至加入一些自己独有的方法, 那么MySet就会成为一个全新的数据结构类,这就是委托模式的意义所在。

但是目前这种写法也有一定弊端,如果接口中待实现方法有几十个甚至上百个,每个都去这样调用辅助对象中的方法的话就会非常麻烦,所以我们就需要使用委托的功能来解决。

Kotlin中委托的关键字是by,只需要在接口声明后面使用by关键字,再加上委托的辅助对象,就可以免去之前所写的一大堆板式的代码了。如下所示:

class MySet<T>(val helperSet: HashSet<T>) : Set<T> by helperSet {
    // 这段代码和上段代码实现的效果是一模一样的
    // 如果我们要对某个方法重新实现,只需要单独重写那一个方法即可
    // 下面代码重写了isEmpty方法以及新增helloWorld方法
    fun helloWorld() = println("Hello World")
    override fun isEmpty() = false
}

二、属性委托

委托属性的核心思想是将一个属性(字段)的具体实现委托给另一个类去完成。下面是委托属性的语法结构:

class MyClass {
    var p by Delegate()
}

这里使用by关键字将p属性的具体实现委托给了Delegate类去完成,当调用p属性的时候会自动调用Delegate类的getValue()方法,当给p属性赋值的时候会自动调用Delegate类的setValue()方法,下面是Delegate类的具体实现:

class Delegate {
    var propValue: Any? = null
    operator fun getValue(myClass: MyClass, prop: KProperty<*>): Any? {
        return propValue
    }
    
    operator fun setValue(myClass: MyClass, prop: KProperty<*>, 
            valueL Any?) {
        propValue = value
   }
}

这是一种标准的代码实现模板,在Delegate类中我们必须实现getValue()和setValue()这两个方法,并且都要使用operator关键字进行声明。

getValue()接收两个参数,第一个参数用于声明该Delegate类的委托功能可以在什么类中使用,这里写成MyClass表示仅可在MyClass类中使用,第二个参数KProperty<*>是Kotlin中的一个属性操作类,另外<*>这种写法表示不知道或者不关心泛型的具体类型,只是为了通过语法编译,类似于Java中的<?>写法,返回值可以声明成任何类型,根据具体实现逻辑写就可以。

三、实现自己的lazy函数

by lazy{}代码块中的代码在一开始并不会执行,只有当变量被首次调用的时候,代码块中的代码才会被执行。实际上by lazy并不是连在一起的关键字,只有by才是关键字,lazy只是一个高阶函数而已,在lazy函数中会创建并返回一个Delegate对象,当我们调用p属性的时候,其实调用的是Delegate对象的getValue()方法,然后getValue()方法中又会调用lazy函数传入的Lambda表达式,这样表达式中的代码就可以得到执行了,并且调用p属性后得到的值就是Lambda表达式最后一行代码的返回值。

class Later<T>(val block: () -> T) {
    val value: Any? = null
    operator fun getValue(any: Any?, prop: KProperty<*>): T {
        if(value == null) {
            value = block()
        }
        return value as T
    }
}

// 为了使它的用法更类似于lazy函数,再定义一个顶层函数
fun <T> later(block: () -> T) = Later(block)

// 替换之前的lazy代码
val uriMatcher by later {
    val matcher = UriMatcher(UriMatcher.NO_MATCH)
    ...
    matcher
}