目录

Kotlin系列(二十五):秘技之可观察属性


一、前言:

  • 我们在上一篇学习了Kotlin的秘技之一懒加载属性的使用及做了一个大胆的猜测。
  • 本章我们将学习基于委托属性而演变出来的非常使用的一个属性-可观察属性;
  • 可观察属性也是Kotlin标准库中提供的工厂方法之一;

二、可观察属性 Observable

  • Delegates.observable()接受两个参数:初始值和修改时处理程序(handler)。

  • 每当我们给属性赋值时会调用该处理程序(在赋值执行)。它有三个参数:被赋值的属性、旧值和新值:

      import kotlin.properties.Delegates
    
      class User {
      		var name: String by Delegates.observable("<no name>") {
      				prop, old, new ->
      				println("$old -> $new")
      		}
      }
    
      fun main(args: Array<String>) {
      		val user = User()
      		user.name = "first"
      		user.name = "second"
      }
    
  • 这个例子输出:

      <no name> -> first
      first -> second
    
  • 如果你想能够截获一个赋值并“否决”它,就使用 vetoable() 取代 observable()。 在属性被赋新值生效之前会调用传递给 vetoable 的处理程序。

把属性储存在映射中

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

      class User(val map: Map<String, Any?>) {
      		val name: String by map
      		val age: Int     by map
      }
    
  • 在这个例子中,构造函数接受一个映射参数:

      val user = User(mapOf(
      		"name" to "John Doe",
      		"age"  to 25
      ))
    
  • 委托属性会从这个映射中取值(通过字符串键——属性的名称):

      println(user.name) // Prints "John Doe"
      println(user.age)  // Prints 25
    
  • 这也适用于 var{:.keyword} 属性,如果把只读的 Map 换成 MutableMap 的话:

      class MutableUser(val map: MutableMap<String, Any?>) {
      		var name: String by map
      		var age: Int     by map
      }
    

局部委托属性(自 1.1 版本起)

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

      fun example(computeFoo: () -> Foo) {
      		val memoizedFoo by lazy(computeFoo)
    
      		if (someCondition && memoizedFoo.isValid()) {
      				memoizedFoo.doSomething()
      		}
      }
    
  • memoizedFoo 变量只会在第一次访问时计算。

  • 如果 someCondition 失败,那么该变量根本不会计算。

属性委托要求

  • 这里我们总结了委托对象的要求。

  • 对于一个只读属性(即 val{:.keyword} 声明的),委托必须提供一个名为 getValue 的函数,该函数接受以下参数:

  • thisRef —— 必须与 属性所有者 类型(对于扩展属性——指被扩展的类型)相同或者是它的超类型;
  • property —— 必须是类型 KProperty<*> 或其超类型。
  • 这个函数必须返回与属性相同的类型(或其子类型)。

  • 对于一个可变属性(即 var{:.keyword} 声明的),委托必须额外提供一个名为 setValue 的函数,该函数接受以下参数:

  • thisRef —— 同 getValue()
  • property —— 同 getValue()
  • new value —— 必须和属性同类型或者是它的超类型。
  • getValue() 或/和 setValue() 函数可以通过委托类的成员函数提供或者由扩展函数提供。

  • 当你需要委托属性到原本未提供的这些函数的对象时后者会更便利。 两函数都需要用 operator 关键字来进行标记。

  • 委托类可以实现包含所需 operator 方法的 ReadOnlyPropertyReadWriteProperty 接口之一。

  • 这俩接口是在 Kotlin 标准库中声明的:

      interface ReadOnlyProperty<in R, out T> {
      		operator fun getValue(thisRef: R, property: KProperty<*>): T
      }
    
      interface ReadWriteProperty<in R, T> {
      		operator fun getValue(thisRef: R, property: KProperty<*>): T
      		operator fun setValue(thisRef: R, property: KProperty<*>, value: T)
      }
    

翻译规则

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

      class C {
      		var prop: Type by MyDelegate()
      }
    
      // 这段是由编译器生成的相应代码:
      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::propKProperty 类型的反射对象,该对象描述 prop 自身。

  • 请注意,直接在代码中引用绑定的可调用引用的语法 this::prop 自 Kotlin 1.1 起才可用。

提供委托(自 1.1 起)

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

  • provideDelegate 的一个可能的使用场景是在创建属性时(而不仅在其 getter 或 setter 中)检查属性一致性。

  • 例如,如果要在绑定之前检查属性名称,可以这样写:

      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<*> 或其超类型。
  • 在创建 MyUI 实例期间,为每个属性调用 provideDelegate 方法,并立即执行必要的验证。

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

      // 检查属性名称而不使用“provideDelegate”功能
      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)
      	 // 创建委托
      }
    
  • 在生成的代码中,会调用 provideDelegate 方法来初始化辅助的 prop$delegate 属性。

  • 比较对于属性声明 val prop: Type by MyDelegate() 生成的代码与上面(当 provideDelegate 方法不存在时)生成的代码:

      class C {
      		var prop: Type by MyDelegate()
      }
    
      // 这段代码是当“provideDelegate”功能可用时
      // 由编译器生成的代码:
      class C {
      		// 调用“provideDelegate”来创建额外的“delegate”属性
      		private val prop$delegate = MyDelegate().provideDelegate(this, this::prop)
      		val prop: Type
      				get() = prop$delegate.getValue(this, this::prop)
      }
    
  • 请注意,provideDelegate 方法只影响辅助属性的创建,并不会影响为 getter 或 setter 生成的代码。