Kotlin 基础 | 委托及其应用

2021-05-28 17:32:24 浏览数 (1)

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

Kotlin 的装饰者模式

装饰者模式和继承拥有相同的目的,都是为了扩展类,只不过它运用了更复杂的方式通:继承 组合。装饰者模式在复用原有类型和行为的基础上为其扩展功能。

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

代码语言:javascript复制
interface Accessory {
    fun name(): String // 配件名字
    fun cost(): Int //  配件价格
    fun type(): String // 配件类别
}

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

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

代码语言:javascript复制
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"
}

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

代码语言:javascript复制
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"
}

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

代码语言:javascript复制
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类:

代码语言:javascript复制
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()接口的实现就可以省略。

惰性初始化一次

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

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

代码语言:javascript复制
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()内部运用的技术,有了它就可以消灭模板代码:

代码语言:javascript复制
class BitmapManager {
    val bitmaps by lazy { loadBitmaps() }
}

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

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

属性委托的三种实现方式

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

代码语言:javascript复制
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()方法。它使用了另一种更加灵活的方式:

代码语言:javascript复制
public inline operator fun <T> Lazy<T>.getValue(thisRef: Any?, property: KProperty<*>): T = value

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

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

代码语言:javascript复制
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):

代码语言:javascript复制
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()方法。

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

代码语言:javascript复制
class Test {
    var str: String by Delegate()
}

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

代码语言:javascript复制
class Test {
    private delegate = Delegate()
    var str : String
        get () = delegate.getValue(this, kProperty)
        set (value: String) = delegate.setValue(this, kProperty, value)
}

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

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

代码语言:javascript复制
val test = Text()
val str = test.str // 等价于 val str = test.delegate.getValue(test, kProperty)
val test.str = str // 等价于 test.delegate.setValue(test, Kproperty, str)

委托应用

更简便地获取传参

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

代码语言:javascript复制
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 值的代码就很模板。可以使用委托隐藏一下细节:

代码语言:javascript复制
// 新建 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
        }
}

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

代码语言:javascript复制
class TestActivity : AppCompatActivity() {
    private val id by Extras("id","0")
}

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

更简便地获取 map 值

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

代码语言:javascript复制
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来存储属性就很合适。

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

代码语言:javascript复制
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接口、重载运算符。

0 人点赞