Kotlin系列(二十五):秘技之可观察属性
- 更多分享:www.catbro.cn
一、前言:
- 我们在上一篇学习了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
方法的ReadOnlyProperty
或ReadWriteProperty
接口之一。 -
这俩接口是在 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::prop
是KProperty
类型的反射对象,该对象描述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 生成的代码。