Kotlin学习之-5.13 代理属性
有一些常见的属性,尽管我们可以每次需要他们的时候手动实现他们,但是最好还是可以实现一次然后随处可用,并把它放到库里。 这些例子包括:
- 懒加载属性:属性值会在第一次访问的时候被计算
- 可观察的属性:监听器会得到属性变化的通知
- 存储在map中的属性,而不是单独给每个属性一个单独的变量
为了覆盖这些情况,Kotlin支持代理属性
class Example {
var p: String by Delegate()
}
语法是:val/var <property name>: <Type> by <expression>
。 在by
之后的表达式就是代理,因为属性对应的get()
和set()
方法会被代理成它的getValue()
和setValue()
方法。属性代理不是必须实现接口,但是他们必须提供getValue()
和setValue()
方法。 例如
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.'")
}
}
荡我们读取p
的时候,他会代理到一个Delegate
对象,然后Delegate
中的getValue()
函数会被调用,因此对象的第一个参数是我们要读取的对象p
,第二个参数持有p
本身的描述。 例如:
val e = Example()
println(e.p)
这会输出
Example@33a17727, thank you for delegating 'p' to me!
类似的,当我们给p
赋值的时候,setValue()
方法会被调用。前两个参数和签名一样,第三个参数持有要被赋值的值:
e.p = "NEW"
这会输出
NEW has been assigned to 'p' in Example@33a17727
代理对象的具体要求在下面会详细描述。
注意从Kotlin v1.1 开始,可以在一个函数或者代码块中声明一个代理属性,它不是必须是一个类的成员。
标准代理
Kotlin标准库提供一些有用的代理工厂方法。
懒函数
lazy()
函数接收一个lambda表达式并且返回一个Lazy<T>
的实例,这个实例可以当做一个实现了懒属性的代理:第一次调用get()
时执行传递给lazy()
函数的lambda表达式并记住它的结果,后续的调用get()
就直接返回记住的结果。
val lazyValue: String by lazy {
println("computed!")
"Hello"
}
fun main(args: Array<String>) {
println(lazyValue)
println(lazyValue)
}
这个例子会输出
computed!
Hello
Hello
默认情况下,懒属性的评估是同步的(synchronized):只会在一个线程中计算它的值,并且所有线程看到的值是相同的。如果不要求代理初始化的同步,那么多个线程都可以同时执行它,传递LazyThreadSafetyMode.PUBLICATION
当做lazy()
函数的一个参数。并且,如果你确认它的初始化总是在一个线程上执行,你可以使用LazyThreadSafeMode.NONE
模式,这样就不会有任何线程安全的保障以及相应的开销。
可观察属性(Observable)
Delegates.observalbe()
接收两个参数:初始值和一个对于修改的回调。每次当我们给属性赋值的时候,赋值完成后,回调都会被调用。 它有三个参数:被赋值的属性,属性的原值,属性的新值。
import kotlin.properties.Delegates
class User {
var name: String by Deleates.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()
。这样传递给vetoble
的回调是在赋值之前调用。
在Map中存储属性
一种常见的情况是在Map中存储属性。当解析JSON或者处理其他动态的事情时经常需要这么做,你可以使用map对象本身作为一个代理属性的代理。
class User(val map: Map<String, Any?>) {
val name: String by map
val age: Int by map
}
在这个例子中,构造器接收一个map:
val user = User(mapOf(
"name" to "John Dow",
"age" to 25
))
代理属性从map中接收值,通过属性的键值对
println(user.name) // 输出"John Doe"
println(user.age) // 输出25
对于var
属性也可以使用MutableMap
class MutableUser(val map: MutableMap<String, Any?>) {
var name: String by map
var age: Int by map
}
局部代理属性(从Kotlin v1.1)
你可以什么局部变量作为代理属性。例如,你可以让一个局部变量延迟加载:
fun example(computeFoo: () -> Foo) {
val memorizedFoo by lazy(computeFoo)
if (someCondition && memorizedFoo.isValid()) {
memorizedFoo.doSomething()
}
}
变量memorizedFoo
的值仅会在第一次被访问时计算。如果someConditon
的结果是‘否’,那么变量值永远都不会被计算。
属性代理的要求
这里我们总结一下代理对象的要求。
对于一个只读属性(例如val
),一个代理必须提供一个名为getValue
的函数,这个函数接收下面两个参数:
thisRef
, 必须和属性拥有者是一样的,或者是它的父类。property
,必须是KProperty<*>
类型或者它的父类。
这个函数返回值的类型必须和属性的类型是一样的,或者是它的子类。
对于可变属性(var
),一个代理必须附加提供一个名为setValue
的函数,这个函数接收下面三个参数:
thisRef
和getValue()
中的一样property
和getValue()
中的一样- new value, 类型必须和属性的类型一样活着是它的父类。
getValue()
和setValue()
函数可以被用作代理类的成员函数或者扩展函数。后者非常帮助,当你想要代理一个对象,但是它并没有提供这些方法。这两个函数都需要用关键字operator
来标记。
代理类可以实现两个接口中的任意一个,ReadOnlyProperty
和ReadWriteProperty
,他们都包含必须的operator
操作符函数。 这些接口在Kotlin标准库中已经定义。
interface ReadOnlyProperty<in R, out T> {
operator fun getValue(thisRef: R, property: KProperty<*>): T
}
interface ReadWriteProperty<in R, out 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
这样的语法参考bound callable reference仅在Kotlin v1.1之后可用.
提供一个代理(Kotlin v1.1之后支持)
通过定义provideDelegate
操作符,你可以扩展创建对象的逻辑,这些对象属性的实现是代理的。如果对象用在by
的右手边,定义provideDelegate
当做一个成员或者扩展函数,这个函数可以被调用来创建属性代理对象。
一个可能的provideDelegate
用例是在创建属性的时候检查属性一致性,而不仅仅是在他的getter 和setter方法。
例如,如果你想要在绑定之前检查属性名,你可以这样写:
class ResourceLoader<T>(id: ResourceID<T>) {
operator fun provideDelegate(
thisRef: MyUI,
prop: KProperty<*>
): ReadOnlyProperty<MyUI, T> {
checkProperty(thisRef, prop.name)
}
private fun checkProperty(thisRef: MyUI, name: String) {
}
}
fun <T> bindResource(id: ResourceID<T>): ResourceLoader<T> { }
class MyUI {
val image by bindResource<ResourceID.image_id)
val text by bindResource<ResourceID.text_id)
}
provideDelegate
的参数是和getValue
的参数是一样的:
thisRef
, 类型必须和属性所有者的类型是一样的或者是他的父类property
,类型必须是KProperty<*>
或者是它的父类
provideDelegate
方法在MyUI
实例创建的时候会为每个属性调用,并且他会立即执行必要的验证。
如果没有在属性和他的代理之间拦截绑定的能力,想要实现同样的功能你必须显式地传递属性名,这样不是很方便。
// 不使用'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()
class C {
val 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
PS,我会坚持把这个系列写完,有问题可以留言交流,也关注专栏Kotlin for Android Kotlin安卓开发