kotlin 中的委托

948 阅读7分钟

经常在 Kotlin 的源码三方库中看到by关键字,这种写法就是委托,主要有两个应用场景,一个是委托类,另一个是委托属性,每个场景中又有不同的用法,我们可以对比 Java 的委托来学习 Kotlin 的委托。

委托(类委托、接口委托)

其实我们在 Java 和 Android 中经常会用到委托,

public class Delegated {

    interface Base{
        void print();
    }
    class BaseImpl implements Base{
        private final int x;

        public BaseImpl(int x) {
            this.x = x;
        }

        @Override
        public void print() {
            System.out.println(x);
        }
    }

    class Derived implements Base{
        private final Base base;

        public Derived(Base base) {
            this.base = base;
        }

        @Override
        public void print() {
            base.print();
        }
    }
}

我们有一个接口Base,一个实现类BaseImpl。假如我们想要在实现类中添加一些方法,但又不想重新写一遍接口实现,第一种我们可以继承BaseImpl,另外一种就是实现接口Base,传入一个实现类的实例,将所有的接口请求都交给实现类的实例来完成。 虽然官方说委托模式已经证明是实现继承的一个很好的替代方式(The Delegation pattern has proven to be a good alternative to implementation inheritance),但选择权还是在大家手上,看情况而定,没有银弹。 那么在 kotlin 中应该怎么写呢?如果我们用 java 的思想来写,无非就是换换关键字,然后一坨模板代码,其实在 kotlin 中是可以通过by关键字零模板代码支持的

interface Base {
    fun print()
}

class BaseImpl(val x: Int) : Base {
    override fun print() {
        println(x)
    }
}

class Derived(b: Base) : Base by b

Derived的超类型列表中的by子句表示b将会在Derived中内部存储, 并且编译器将生成转发给b的所有Base的方法。 这个就是 kotlin 中的委托,有的地方也叫委托类或者委托接口。 这里有一点需要注意下,覆盖(override)是符合预期的:编译器会使用 override 覆盖的实现而不是委托对象中的.

interface Base {
    fun printMessage()
    fun printMessageLine()
}

class BaseImpl(val x: Int) : Base {
    override fun printMessage() { println(x) }
    override fun printMessageLine() { println(x) }
}

class Derived2(b: Base) : Base by b {
    override fun printMessage() { println("abc") }
}

fun main() {
    val b = BaseImpl(10)
    Derived2(b).printMessage()
    Derived2(b).printMessageLine()
}

我们在Derived2中覆写了printMessage这个方法,那么在调用的时候,就是用的我们覆写的方法。

属性委托

class Example {
    var p: String by Delegate()
}

语法是:val/var <属性名>: <类型> by <表达式>。在by后面的表达式是该委托, 因为属性对应的get()(与set())会被委托给它的getValue()setValue()方法。 属性的委托不必实现接口,但是需要提供一个getValue()函数(对于var属性还有setValue())。 先从最简单的委托开始,最后再看自定义委托。

标准委托

借用官网的一个例子

class MyClass {
   var newName: Int = 0
   @Deprecated("Use 'newName' instead", ReplaceWith("newName"))
   var oldName: Int by this::newName
}
fun main() {
   val myClass = MyClass()
   // 通知:'oldName: Int' is deprecated.
   // Use 'newName' instead
   myClass.oldName = 42
   println(myClass.newName) // 42
}

这是一种最简单的委托方式。通过查看对应的 java 代码,发现其实就是对同一个成员变量的读写

public final class MyClass {
   private int newName;

   public final int getNewName() {
      return this.newName;
   }

   public final void setNewName(int var1) {
      this.newName = var1;
   }

   /** @deprecated */
   public final int getOldName() {
      return this.newName;
   }

   /** @deprecated */
   public final void setOldName(int var1) {
      this.newName = var1;
   }
}

MyClass中的四个方法都是对newName这个字段的读写。

除此之外,委托属性可以是:

  • 顶层属性
  • 同一个类的成员或扩展属性
  • 另一个类的成员或扩展属性

比如

var topLevelInt: Int = 0
class ClassWithDelegate(val anotherClassInt: Int)

class MyClass(var memberInt: Int, val anotherClassInstance: ClassWithDelegate) {
    var delegatedToMember: Int by this::memberInt//同一个类的成员
    var delegatedToTopLevel: Int by ::topLevelInt//顶层属性

    val delegatedToAnotherClass: Int by anotherClassInstance::anotherClassInt//另一个类的成员
}
var MyClass.extDelegated: Int by ::topLevelInt//顶层属性

这种委托方式在我们做版本升级修改字段时是挺常用的,将旧字段委托给新字段,并将旧字段标记为过时。

懒加载委托

这种方式就是当我们首次访问这个属性的时候才会去初始化这个属性,从而避免不必要的资源消耗,和我们用 java 写单例模式的懒加载是一样的。 只会在首次访问的时候初始化这个属性,然后缓存起来,下次访问时直接返回。

val lazyValue: String by lazy {
    println("computed!")
    "Hello"
}

fun main() {
    println(lazyValue)
    println(lazyValue)
}

这里的 lazy 是一个高阶函数:

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

public actual fun <T> lazy(mode: LazyThreadSafetyMode, initializer: () -> T): Lazy<T> =
    when (mode) {
        LazyThreadSafetyMode.SYNCHRONIZED -> SynchronizedLazyImpl(initializer)
        LazyThreadSafetyMode.PUBLICATION -> SafePublicationLazyImpl(initializer)
        LazyThreadSafetyMode.NONE -> UnsafeLazyImpl(initializer)
    }

可以看到,lazy接收一个 mode 参数,如果没有传入的话,默认是SynchronizedLazyImpl线程安全的:该值只在一个线程中计算,但所有线程都会看到相同的值。如果初始化委托的同步锁不是必需的,这样可以让多个线程同时执行,那么将LazyThreadSafetyMode.PUBLICATION作为参数传给 lazy()。 如果我们确定初始化将总是发生在与属性使用位于相同的线程, 那么可以使用LazyThreadSafetyMode.NONE模式。它不会有任何线程安全的保证以及相关的开销。 所以这个参数的选择也要看具体应用场景。

可观察委托

如果我们想要观察属性值的变化,可以使用Delegates.observable(),它接受两个参数:初始值与修改时处理程序(handler)。

class  ObservableItem{
    var name :String by Delegates.observable("initialValue"){
        prop,old,new->
        println("$prop  $old -> $new")
    }
}

当我们给name赋值的时候,就会触发传入的处理程序,但这里我们只能观察到赋值,但并不能做拦截,如果想要截获取值并否决,可以使用vetoable()

可否决委托

如果我们想在观察属性值变化的同时决定是否使用新的值,可以使用Delegates.vetoable,同样的,它也接受两个参数:它接受两个参数:初始值与修改时处理程序(handler)。只不过这里的 handler 需要返回一个布尔值,告诉程序是否使用新值。

class VetoableItem{
    var name :String by Delegates.vetoable("initialValue"){
        prop,old,new->
        println("$prop  $old -> $new")
        new.length > 3
    }
}

fun main(){
    val vetoableItem = VetoableItem()
    println(vetoableItem.name)
    vetoableItem.name = "123"
    println(vetoableItem.name)
    vetoableItem.name = "1234"
    println(vetoableItem.name)
}
//输出
// initialValue
// var com.huangyuanlove.VetoableItem.name: kotlin.String  initialValue -> 123
// initialValue
// var com.huangyuanlove.VetoableItem.name: kotlin.String  initialValue -> 1234
// 1234

在这里,只有当new的长度大于 3 时,我们才会将new赋值给name

将属性储存在映射中

一个常见的用例是在一个映射(map)里存储属性的值。 这经常出现在像解析 JSON 或者执行其他“动态”任务的应用中。 在这种情况下,你可以使用映射实例自身作为委托来实现委托属性。

class MapItem(map: Map<String,Any?>){
    val name: String by map
    val age:Int by map
    val address:String by map
}
fun main(){
  val map:Map<String,Any?> = mapOf(
    "name" to "xuan",
    "age" to 18,
  )
  val mapItem = MapItem(map)
  println("${mapItem.name}  ${mapItem.age}")
  println("${mapItem.name}  ${mapItem.age}  ${mapItem.address}")
}

这里需要注意,假如我们传入的map里面没有对应属性,当程序运行时,这个属性没有被使用是没问题的,比如上面打印nameage。但是当我们使用这个属性的时候,就是上面打印address,会抛出异常Key address is missing in the map..另外一方面,我们将传入的 map 的值声明为了可空,这就意味着在调用出传入了空值,比如"address" to null,,代码是可以运行的,但对address这个属性做处理的时候会报空指针异常,这些都是需要额外注意的地方。 还有一点需要注意,如果是对于var属性,需要将Map替换成MutableMap,但是这样的话它们两个可是双向绑定的哟,比如下面这种

class MutableMapItem(map:MutableMap<String,Any?>){
    var name: String by map
    var age: Int by map
    var address: String by map
}
fun main() {
      val map:MutableMap<String,Any?> = mutableMapOf(
        "name" to "xuan",
        "age" to 18,
        "address" to "beijing"
    )
    val mutableMapItem = MutableMapItem(map)
    println("${mutableMapItem.name}  ${mutableMapItem.age} ${mutableMapItem.address}")
    println(map)
    mutableMapItem.name = "huang"
    println("${mutableMapItem.name}  ${mutableMapItem.age} ${mutableMapItem.address}")
    println(map)
    mutableMapItem.name = "yuan"
    println("${mutableMapItem.name}  ${mutableMapItem.age} ${mutableMapItem.address}")
    println(map)
}
//输出
// xuan  18 beijing
// {name=xuan, age=18, address=beijing}
// huang  18 beijing
// {name=huang, age=18, address=beijing}
// yuan  18 beijing
// {name=yuan, age=18, address=beijing}

局部属性委托

可以将局部变量声明为委托属性。 例如,你可以使一个局部变量惰性初始化:

fun example(computeFoo: () -> Int) {
    val memoizedFoo by lazy(computeFoo)
    val someCondition = false

    if (someCondition && memoizedFoo>0 ) {
        println(memoizedFoo+1)
    }
}

memoizedFoo变量只会在第一次访问时计算。 如果someCondition失败,那么该变量根本不会计算。

自定义委托

先看一下自定义委托的要求有哪些,示例是这样的

class Resource

class Owner {
    var varResource: Resource by ResourceDelegate()
}

class ResourceDelegate(private var resource: Resource = Resource()) {
    operator fun getValue(thisRef: Owner, property: KProperty<*>): Resource {
        return resource
    }
    operator fun setValue(thisRef: Owner, property: KProperty<*>, value: Any?) {
        if (value is Resource) {
            resource = value
        }
    }
}

总结一下

  1. 对于var修饰的属性,我们必须要有getValuesetValue这两个方法,同时,这两个方法必须用operator关键字修饰。
  2. 由于varResourceOwner,因此getValuesetValue这两个方法中的thisRef的类型,必须要是Owner或者是Owner的父类。一般来说,这三处的类型是一致的,当我们不确定委托属性会处于哪个类的时候,就可以将thisRef的类型定义为Any?
  3. 由于委托的属性是Resource类型,那么对于自定义委托中的getValuesetValue参数及返回值需要是String类型或者是它的父类

我们可以把上面的代码当成模板代码,都是这样写就好了。如果觉得麻烦,可以使用标准库中的接口ReadOnlyPropertyReadWriteProperty将委托创建为匿名对象,而无需创建新类。它们提供所需的方法:getValue()ReadOnlyProperty中声明;ReadWriteProperty扩展了它并添加了setValue()。这意味着可以在需要ReadOnlyProperty时传递 ReadWriteProperty。 比如像这样

fun resourceDelegate(resource: Resource= Resource()) :ReadWriteProperty<Owner,Resource> =
    object:ReadWriteProperty<Owner,Resource>{
        var curValue = resource
        override fun getValue(thisRef: Owner, property: KProperty<*>): Resource=curValue
        
        override fun setValue(thisRef: Owner, property: KProperty<*>, value: Resource) {
            curValue = value
        }

    }

class Owner {
    val readOnlyResource: Resource by resourceDelegate()  // ReadWriteProperty as val
    var readWriteResource: Resource by resourceDelegate()
}

提供委托

通过定义 provideDelegate 操作符,可以扩展创建属性实现所委托对象的逻辑。 如果 by 右侧所使用的对象将 provideDelegate 定义为成员或扩展函数, 那么会调用该函数来创建属性委托实例。比如在初始化之前检查一致性。

class ResourceDelegate<T> : ReadOnlyProperty<MyUI, T> {
    override fun getValue(thisRef: MyUI, property: KProperty<*>): T { ... }
}

class ResourceLoader<T>(id: ResourceID<T>) {
    operator fun provideDelegate(
            thisRef: MyUI,
            prop: KProperty<*>
    ): ReadOnlyProperty<MyUI, T> {
        checkProperty(thisRef, prop.name)
        // 创建委托
        return ResourceDelegate()
    }

    private fun checkProperty(thisRef: MyUI, name: String) { …… }
}

class MyUI {
    fun <T> bindResource(id: ResourceID<T>): ResourceLoader<T> { …… }

    val image by bindResource(ResourceID.image_id)
    val text by bindResource(ResourceID.text_id)
}

provideDelegate的参数与getValue的相同:

  • thisRef必须与属性所有者类型(对于扩展属性必须是被扩展的类型)相同或者是它的超类型;
  • property必须是类型KProperty<*>或其超类型。

参考:

委托
Delegation