Kotlin 基础 | 委托及其应用

2,960 阅读7分钟

委托是常见的模式,它和编程语言无关,即把本来自己做的事情委托给另一个对象去做。装饰者模式和代理模式都通过委托复用了行为。Kotlin 在语言层面支持了委托,这一篇结合实例介绍一下 Kotlin 的委托。

Kotlin 的装饰者模式

装饰者模式和继承拥有相同的目的,都是为了扩展类,只不过它运用了更复杂的方式通:继承 + 组合。装饰者模式在复用原有类型和行为的基础上为其扩展功能。关于装饰者模式和代理模式的详细分析可以点击使用组合的设计模式 | 追女孩要用的远程代理模式

下面是装饰者模式的实例:

interface Accessory {
    fun name(): String // 配件名字
    fun cost(): Int //  配件价格
    fun type(): String // 配件类别
}

这个接口用来描述一个抽象的配件,一个具体的配件需要实现三个方法,分别来定义配件名字、价格、类别。

羽毛、戒指、耳环是3个具体的配件,它的实现如下:

class Feather: Accessory{
    override fun name(): String = "Feather"
    override fun cost(): Int  = 20
    override fun type(): String  = "body accessory"
}

class Ring: Accessory{
    override fun name(): String = "Ring"
    override fun cost(): Int  = 30
    override fun type(): String  = "body accessory"
}

class Earrings: Accessory{
    override fun name(): String = "Earrings"
    override fun cost(): Int  = 15
    override fun type(): String  = "body accessory"
}

现需要新增羽毛戒指和羽毛耳环,按照继承的思想可以这样实现:

class FeatherRing: Accessory{
    override fun name(): String = "FeatherRing"
    override fun cost(): Int  = 35
    override fun type(): String  = "body accessory"
}

class FeatherEarrings: Accessory{
    override fun name(): String = "FeatherEarrings"
    override fun cost(): Int  = 45
    override fun type(): String  = "body accessory"
}

这样写的缺点是只复用了类型,没复用行为。每次新增类型的时候都得新增一个子类,会造成子类膨胀。若改用装饰者模式,则可以减少一个子类:

class Feather(private var accessory: Accessory) : Accessory {
    override fun name(): String = "Feather" + accessory.name()
    override fun cost(): Int = 20 + accessory.cost()
    override fun type(): String  = accessory.type()
}

现在羽毛戒指和耳环分别可以这样表达Feather(Ring())Feather(Earrings())

Feather运用组合持有了一个抽象的配件,这样被注入配件的行为就得以复用。name()cost()在复用行为的基础上追加了新的功能,而type()直接将实现委托给了accessory

运用 Kotlin 的委托语法可以进一步简化Feather类:

class Feather(private var accessory: Accessory): Accessory by accessory {
    override fun name(): String = "Feather" + accessory.name()
    override fun cost(): Int = 20 + accessory.cost()
}

by 关键词出现在类名后面,表示类委托,即把类的实现委托一个对象,该对象必须实现和类相同的接口,在这里是Accessory接口。使用by的好处是消灭模板代码,就如上面所示,type()接口的实现就可以省略。

惰性初始化一次

惰性初始化也是一种常见的模式:延迟对象的初始化,直到第一次访问它。当初始化消耗大量资源,惰性初始化显得特别有价值。

支持属性是一种实现惰性初始化的惯用技术:

class BitmapManager {
    // 支持属性用于存储一组 Bitmap
    private var _bitmaps: List<Bitmap>? = null
    // 供外部访问的一组 Bitmap
    val bitmaps: List<Bitmap>
        get() {
            if (_bitmaps == null) {
                _bitmaps = loadBitmaps()
            }
            return _bitmaps!!
        }
}

支持属性_bitmaps是私有的,它用来存储一组 Bitmap,而另一个同样类型的bitmaps用来提供一组 Bitmap 的访问。

这样只有当第一次访问BitmapManager.bitmaps时,才会去加载 Bitmap。第二次访问时,也不会重新加载 Bitmap,可直接返回_bitmap

上面这段代码就是 Kotlin 预定义函数lazy()内部运用的技术,有了它就可以消灭模板代码:

class BitmapManager {
    val bitmaps by lazy { loadBitmaps() }
}

这里的关键词by出现在属性名后面,表示属性委托,即将属性的读和写委托给另一个对象,被委托的对象必须满足一定的条件:

  1. 对于 val 修饰的只读变量进行属性委托时,被委托的对象必须实现getValue()接口,即定义如何获取变量值。
  2. 对于 var 修饰的读写变量进行属性委托时,被委托对象必须实现getValue()setValue()接口,即定义如何读写变量值。

属性委托的三种实现方式

lazy()方法的返回值是一个Lazy对象:

public actual fun <T> lazy(initializer: () -> T): Lazy<T> = SynchronizedLazyImpl(initializer)

public interface Lazy<out T> {
    public val value: T
    public fun isInitialized(): Boolean
}

Lazy类并没有直接实现getValue()方法。它使用了另一种更加灵活的方式:

public inline operator fun <T> Lazy<T>.getValue(thisRef: Any?, property: KProperty<*>): T = value

getValue()被声明为Lazy类的扩展函数。这是 Kotlin 独有的在类体外为类新增功能的特性。在原有类不能被修改的时候,特别好用。

除了扩展函数,还有另外两种方式可以实现被委托类(假设代理的类型为 String):

class Delegate {
    operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
        return "Delegate"
    }

    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
    }
}

这种方式新建了一个代理类,并且在类中通过关键词operator重载了getValue()setValue()这两个运算符,分别对应取值和设置操作。

最后一种方式如下(假设代理的类型为 String):

class Delegate : ReadWriteProperty<Any?, String> {
    override fun getValue(thisRef: Any?, property: KProperty<*>): String {
        return "Delegate"
    }

    override fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
    }
}

即实现ReadWriteProperty接口中的getValue()setValue()方法。

然后就可以像这样使用代理类:

class Test {
    var str: String by Delegate()
}

属性委托背后的实现如下:

class Test {
    private delegate = Delegate()
    var str : String
        get () = delegate.getValue(this, kProperty)
        set (value: String) = delegate.setValue(this, kProperty, value)
}

新建的Delegate类会被存储到一个支持属性delegate中,委托属性的设置和取值方法的实现全权委托给代理类。

委托之后,当访问委托属性时就好比在调用代理类的方法:

val test = Text()
val str = test.str // 等价于 val str = test.delegate.getValue(test, kProperty)
val test.str = str // 等价于 test.delegate.setValue(test, Kproperty, str)

委托应用

1. 更简便地获取传参

委托可以隐藏细节,特别是当细节是一些模板代码的时候:

class TestFragment : Fragment() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val id = arguments?.getString("id") ?: ""
    }
}

class KotlinActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val id = intent?.getStringExtra("id") ?: ""
    }
}

获取传递给 Activity 或 Fragment 值的代码就很模板。可以使用委托隐藏一下细节:

// 新建 Extras 类作为被委托类
class Extras<out T>(private val key: String, private val default: T) {
    // 重载取值操作符
    operator fun getValue(thisRef: Any, kProperty: KProperty<*>): T? =
        when (thisRef) {
            // 获取传递给 Activity 的参数
            is Activity -> { thisRef.intent?.extras?.get(key) as? T ?: default }
            // 获取传递给 Fragment 的参数
            is Fragment -> { thisRef.arguments?.get(key) as? T ?: default }
            else -> default
        }
}

然后就可以像这样使用委托:

class TestActivity : AppCompatActivity() {
    private val id by Extras("id","0")
}

class TestFragment : Fragment() {
    private val id by Extras("id","0")
}

2. 更简便地获取 map 值

有些类的属性不是固定的,而是有时多,有时少,即动态的,比如:

class Person {
    private val attrs = hashMapOf<String, Any>()
    fun setAttrs( key: String, value: Any){
        attrs[key] = value
    }
    val name: String
        get() = attrs["name"]
}

有些Person有孩子,有些没有,所以不同Person实例拥有的属性集是不同的。这种场景用Map来存储属性就很合适。

上述代码可以用委托简化:

class Person {
    private val attrs = hashMapOf<String, Any>()
    fun setAttrs( key: String, value: Any){
        attrs[key] = value
    }
    val name: String by attrs
}

name的获取委托给一个 map 对象。神奇之处在于,甚至都不需要指定key就可以正确地从 map 中获取 name 属性值。这是因为 Kotlin 标准库已经为 Map 定义了getValue()setValue()扩展函数。属性名将自动作用于 map 的键。

总结

  1. Kotlin 委托分为类委托属性委托。它们都通过关键词by来进行委托。
  2. 类委托可以用简洁的语法将类的实现委托给另一个对象,以减少模板代码。
  3. 属性委托可以将对属性的访问委托给另一个对象,以减少模板代码并隐藏访问细节。
  4. 属性委托有三种实现方式,分别是扩展方法、实现ReadWriteProperty接口、重载运算符。

推荐阅读