Kotlin中的代理

1,379 阅读5分钟

Kotlin Delegated Properties: kotlinlang.org/docs/delega…

Kotlin Delegation: kotlinlang.org/docs/delega…

Kotlin 从语言层面提供了能够快速实现 代理模式 的能力,以减少模版代码的出现。

设计模式中的委托模式

委托模式的定义是:有两个对象参与处理同一个请求,接受请求的对象将请求委托给另一个对象来处理。

简单的例子:

 class RealPrinter { // the "delegate" 真正执行逻辑的对象(处理请求的对象)
     void print() { 
       System.out.print("something"); 
     }
 }

 class Printer { // the "delegator" 
     RealPrinter p = new RealPrinter(); // create the delegate 通过delegate去执行逻辑(接受请求的对象)
     void print() { 
       p.print(); // delegation
     } 
 }
	public class Main {
     // to the outside world it looks like Printer actually prints.
     public static void main(String[] args) {
         Printer printer = new Printer();
         printer.print();
     }
 }

其中 RealPrinter 是真正执行逻辑的对象,称之为 delegate;Printer 是接受请求的对象,称之为 delegator。

类通过代理实现接口(实现继承)

委托模式已被证明是实现继承的一个很好的替代方案,在 Kotlin 中,类可以通过将其所有公共成员委托给指定对象来实现一个接口,结合上面的例子,逐步分析类是如何通过代理实现接口的。

首先,将上面的设计模式转换为 Kotlin 代码:

 class RealPrinter {
     fun print() { ... }
 }

 class Printer {
     val p = RealPrinter()
     fun print() { 
       p.print()
     } 
 }

 fun main() {
   	 val printer = Printer()
   	 printer.print()
 }

Kotlin 中提供了 by 关键字,来表示代理,将上述的代码转换为用 by 关键字的形式表示:

class RealPrinter {
    fun print() {
        print("something")
    }
}

class Printer(p: RealPrinter): RealPrinter by p // by 前的 RealPrinter 报错,Only interfaces can be delegated to

正常我们仿照上面的逻辑,应该是这样的,但是这个时候编译器会报错

Only interfaces can be delegated to

这个意思是 委托仅限于接口。为什么Kotlin中只有接口可以委托呢?抽象类或者普通的类为什么不可以呢?

这是因为当您委托一个接口时,该类仍然实现该接口。所以为了一致性,如果你可以委托一个类,它应该以同样的方式工作。例如:

class A(x: Int) {
  fun foo() = x
}

class B(val a: A) : A by a

编译为:

class B(val a: A) : A {
  override fun foo() = a.foo()
}

这样是不正常的,因为:

  • foo 函数不是 open 的,不能被重写

  • 你需要调用一个 A 的构造方法,即 B 如果继承自 A,则 B 的定义应该是

    class B(val a: A) : A(x)
    

    很明显,如果继承自 A ,没有参数可以作为 A 的构造参数。

  • 第三点是 equals 和 hashCode 方法,它们是委托的吗?

综上所述,Kotlin 将类的代理,限定为接口。所以通过by关键字实现上述的例子,首先需要定义一个接口:

interface Print {
    fun print()
}

class RealPrinter() : Base {
    override fun print() { ... }
}

class Printer(p: RealPrinter) : Print by p

fun main() {
    val p = RealPrinter()
    Printer(p).print()
}

对于上述代码,首先定义了一个 Print 接口,用来作为代理的接口;它的实现类是 RealPrinter ;通过 by 关键字指定 Printer 的成员 p 为 delegate。在真正的调用过程中,我们要先创建一个真正处理请求的类型为 RealPrinter 的对象,然后,通过接受请求的 delegator 对象 -- Printer 实例,去调用 print 函数。

如果你在 Printer (委托类) 中重写了 print 函数, 将根据 Printer 中的 print 函数逻辑执行打印,而不是 RealPrinter 对象 p 的 print 逻辑。这种情况下并没有达到代理的效果,它调用的是自己的 print 方法,而非代理类型 RealPrinter 的实例 p 的 pirnt 方法。

代理属性

对于一些可以定义为常量的属性,通常我们只需要赋值一次即可,例如:

  • Lazy properties:仅在第一次访问时计算它的值。
  • Observable properties: 监听器会收到有关此属性更改的通知。
  • Map 中存储的强引用类型对象,而不是每个属性的单独字段。

为了覆盖上述场景,Kotlin 提供了代理属性:

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

语法规则为:

 val/var <property name>: <Type> by <expression>

by 后面的表达式是一个代理实现表达式,该属性的getter和setter会被委托给 expression 的 getValue()/setValue()方法实现。即最终实际调用的是 expression 的getValue()/setValue()方法。

getValue()/setValue()方法一般为:

 operator fun getValue(thisRef: Any?, property: KProperty<*>): Type
 
 operator fun setValue(thisRef: Any?, property: KProperty<*>, value: Type)

属性代理不需要实现一个interface,但他们需要提供一个 getValue 函数(和一个 setValue 函数)。

上面的 Example 类中的属性 p 的代理类的实现为:

import kotlin.reflect.KProperty

class Delegate {
  	// 参数和修饰符都是默认实现的
    operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
        return "$thisRef, thank you for delegating '${property.name}' to me!"
    }

    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
        println("$value has been assigned to '${property.name}' in $thisRef.")
    }
}

当你访问一个代理对象时Delegate的属性p时,Delegate 的 getValue 函数会被调用。它的第一个参数是您从中读取 p 的对象,第二个参数包含对属性 p 本身的描述。

Kotlin Standard Library 提供的代理实现

Kotlin standard library 提供了几种常用代理的工厂方法。

Lazy 属性

lazy()是一个带有一个 lambda 表达式参数并且返回了一个Lazy<T>实例的函数,可以作为实现lazy 属性的委托。第一次调用get()会通过lazy()执行 lambda 表达式并且记录结果。后续调用get()会直接返回第一次记录的结果。

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

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

默认的,lazy 属性是同步的:属性的值仅在一个线程计算,所有线程会观测到同样的值。

如果初始化委托的同步不需要允许多个线程同时执行,请将 LazyThreadSafetyMode.PUBLICATION 作为参数传递给lazy()

如果您确定初始化将始终发生在与您使用该属性的线程相同的线程中,您可以使用 LazyThreadSafetyMode.NONE。它不会产生任何线程安全保证和相关开销。

Observable 属性

Delegates.observable()有两个参数,初始值和监听变化的 block 。

inline fun <T> observable(
    initialValue: T,
    crossinline onChange: (property: KProperty<*>, oldValue: T, newValue: T) -> Unit
): ReadWriteProperty<Any?, T>

每次接收到属性变化,都会调用 onChange ,它有三个参数

  • property: KProperty,属性的描述
  • oldValue:旧的 value
  • newValue: 新的 value
import kotlin.properties.Delegates

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

fun main() {
    val user = User()
    user.name = "first"
    user.name = "second"
}

如果你想拦截回调并进行逻辑处理,使用vetoable()而不是observable()。传递给 vetoable 的处理逻辑将在分配新属性值之前被调用。

代理到另一个属性

一个属性可以代理它的 getter 和 setter 给另一个属性,这种委托可用于顶级和类属性(成员和扩展)。这个代理属性可以是:

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

代理一个属性到另一个属性,在委托名称中使用::修饰符,例如 this::delegate

// 顶级属性
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

这可能很有用,例如,当您想以向后兼容的方式重命名属性时:引入新属性,使用注释对旧属性进行@Deprecated注释,并委托其实现。

在Map中存储属性

class User(val map: Map<String, Any?>) {
    val name: String by map
    val age: Int by map
}

fun main() {
    val user = User(mapOf(
        "name" to "John Doe",
        "age"  to 25
    ))
    println(user.name) // Prints "John Doe"
    println(user.age)  // Prints 25
}

这是一个简单的示例,将属性代理给map,User 的 name 和 age 就可以通过 map 去获取值。

如果 map 中 不存在和属性名相同的 key ,就会抛出异常:

Exception in thread "main" java.util.NoSuchElementException: Key age is missing in the map.

临时代理属性

你可以声明一个临时属性作为代理属性,例如,你可以创建一个本地 lazy 属性:

fun example(computeFoo: () -> Foo) {
    val memoizedFoo by lazy(computeFoo)

    if (someCondition && memoizedFoo.isValid()) {
        memoizedFoo.doSomething()
    }
}

memoizedFoo 变量的值只会在第一次访问时计算。如果 someCondition 等于 false,这个属性则始终不会被计算。

属性代理的要求

对于一个只读属性来说,一个 delegate 应该提供一个名为 getValue()的 operator 函数,并且该函数带有以下参数:

  • thisRef,必须与属性所有者的类型或其父类型相同。
  • property,必须是 KProperty<*> 类型或它的父类型。

返回值:getValue()必须返回和被代理的属性相同的类型(或其子类型)。

class Resource

class Owner {
    val valResource: Resource by ResourceDelegate()
}

class ResourceDelegate {
    operator fun getValue(thisRef: Owner, property: KProperty<*>): Resource {
        return Resource()
    }
}

同样的,setValue()函数同样要求以下参数:

  • thisRef,必须与属性所有者的类型或其父类型相同。
  • property,必须是 KProperty<*> 类型或它的父类型。
  • value:必须与被代理属性(或其超类型)具有相同的类型。
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
        }
    }
}

getValue() / setValue()函数可以作为委托类的成员函数或扩展函数提供。当您需要将属性委托给最初不提供这些功能的对象时,使用扩展函数很方便。这两个函数都需要用 operator 关键字标记。

匿名对象代理

您可以使用 Kotlin 标准库中的接口ReadOnlyPropertyReadWriteProperty将委托创建为匿名对象,而无需创建新类。它们提供了所需的方法:

  • getValue()ReadOnlyProperty 中声明;

  • ReadWriteProperty 对其进行了扩展并添加了setValue()

因为是拓展关系,所以你也可以在需要 ReadOnlyProperty 时传递 ReadWriteProperty

fun resourceDelegate(): ReadWriteProperty<Any?, Int> =
    object : ReadWriteProperty<Any?, Int> {
        var curValue = 0
        override fun getValue(thisRef: Any?, property: KProperty<*>): Int = curValue
        override fun setValue(thisRef: Any?, property: KProperty<*>, value: Int) {
            curValue = value
        }
    }

val readOnly: Int by resourceDelegate()  // ReadWriteProperty as val
var readWrite: Int by resourceDelegate()

代理属性的转换规则(底层实现)

在底层,Kotlin 编译器为每个委托属性生成一个辅助属性,然后委托给它。例如,对于属性 prop,它会生成隐藏属性 prop$delegate,而访问器的代码只是简单地委托给这个附加属性:

class C {
    var prop: Type by MyDelegate()
}

// this code is generated by the compiler instead:
class C {
    private val prop$delegate = MyDelegate()
    var prop: Type
        get() = prop$delegate.getValue(this, this::prop)
        set(value: Type) = prop$delegate.setValue(this, this::prop, value)
}

Kotlin 编译器在参数中提供了关于 prop 的所有必要信息:

  • 第一个参数 this 引用外部类 C 的一个实例,
  • this::prop 是描述 prop 本身的 KProperty 类型的反射对象。

代理到另一个属性的转换规则(底层实现)

当委托给另一个属性时,Kotlin 编译器会立即生成对引用属性的访问。这意味着编译器不会生成字段 prop$delegate。这种优化有助于节省内存。

class C<Type> {
    private var impl: Type = ...
    var prop: Type by ::impl
}

prop 变量的属性访问器直接调用 impl 变量,跳过了委托属性的 getValue 和 setValue 运算符,因此不需要 KProperty 引用对象。

对于上面的代码,编译器生成以下代码:

class C<Type> {
    private var impl: Type = ...

    var prop: Type
        get() = impl
        set(value) {
            impl = value
        }

    fun getProp$delegate(): Type = impl // This method is needed only for reflection
}

提供一个delegate

通过定义名为 provideDelegate 的 operator 函数,您可以扩展用于创建委托属性实现的对象的逻辑。

如果 by 右侧使用的对象将 provideDelegate 定义为成员函数或扩展函数,则将调用该函数来创建属性委托实例。

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)
        // create delegate
        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

在创建 MyUI 实例期间为每个属性调用 provideDelegate 方法,并立即执行必要的验证。

如果没有这种拦截属性与其委托之间绑定的能力,要实现相同的功能,您必须显式传递属性名称,这不是很方便:

// Checking the property name without "provideDelegate" functionality
class MyUI {
    val image by bindResource(ResourceID.image_id, "image")
    val text by bindResource(ResourceID.text_id, "text")
}

fun <T> MyUI.bindResource(
        id: ResourceID<T>,
        propertyName: String
): ReadOnlyProperty<MyUI, T> {
    checkProperty(this, propertyName)
    // create delegate
}

在生成的代码中,调用 provideDelegate 方法来初始化辅助的 prop$delegate 属性。将属性声明 val prop: Type by MyDelegate() 的生成代码与上面生成的代码进行比较(当 provideDelegate 方法不存在时):

class C {
    var prop: Type by MyDelegate()
}

// this code is generated by the compiler
// when the 'provideDelegate' function is available:
class C {
    // calling "provideDelegate" to create the additional "delegate" property
    private val prop$delegate = MyDelegate().provideDelegate(this, this::prop)
    var prop: Type
        get() = prop$delegate.getValue(this, this::prop)
        set(value: Type) = prop$delegate.setValue(this, this::prop, value)
}

请注意,provideDelegate 方法仅影响辅助属性的创建,不会影响为 getter 或 setter 生成的代码。

使用标准库中的 PropertyDelegateProvider 接口,您可以创建委托而无需创建新类。

val provider = PropertyDelegateProvider { thisRef: Any?, property ->
    ReadOnlyProperty<Any?, Int> {_, property -> 42 }
}
val delegate: Int by provider