Kotlin Reference: Delegated Properties

  对于一些通用的属性,比起每次都在需要的时候进行手动实现,更好的方法是实现一次并把它放进库里,此类属性如:

  • 延迟(Lazy)属性:仅在第一次访问时进行计算,
  • 可观察(Observable)属性:属性发生变动时通知监听者,
  • 在映射(Map)中存储属性,而不是把属性存储到单独的字段。

  Kotlin 提供了委托属性(Delegated Property)来满足此种(及其他)场景:

[code lang=”kotlin”]class Example {
var p: String by Delegate()
}[/code]

  委托属性的语法为 val/var <property name>: <Type> by <expression>by 后面的表达式是委托(Delegate),对应属性(var p)的 get()(和 set())委托给了 DelegategetValue()(和 setValue())方法。属性委托不需要实现特定接口,但必需要提供 getValue()(对于 var 属性还需要 setValue())方法,如:

[code lang=”kotlin”]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.'”)
}
}[/code]

  当我们读取委托给了 Delegatep 时,DelegategetValue() 会被调用,getValue() 的第一个参数是 p 来源的对象,第二个参数是 p 本身的描述(如可以从中获得属性的名称),例如:

[code lang=”kotlin”]val e = Example()
println(e.p)[/code]

输出为:

Example@33a17727, thank you for delegating ‘p’ to me!

  类似的,当我们给 p 赋值时,setValue() 会被调用,setValue() 的前两个参数与 getValue() 一样,第三个参数是要赋的值:

[code lang=”kotlin”]e.p = “NEW”[/code]

上面例子的输出为:

NEW has been assigned to ‘p’ in Example@33a17727.

  委托对象的具体规范和要求见下面

  注意从 Kotlin 1.1 起,委托对象可以声明在函数和代码块中,而不必作为类的成员,如下面的例子

Standard Delegates

  Kotlin 标准库提供了多种常用委托的工厂方法。

Lazy

  lazy() 函数接受一个 Lambda 表达式并返回一个 Lazy<T> 实例,作为实现了延迟初始化属性的委托:首次调用 get() 时会运行传递给 lazy() 的 lambda 表达式,并存储结果,之后调用 get() 就会直接返回已存储的结果。

[code lang=”kotlin”]val lazyValue: String by lazy {
println(“computed!”)
“Hello”
}

fun main(args: Array) {
println(lazyValue)
println(lazyValue)
}[/code]

上面例子的输出为:

computed!
Hello
Hello

  默认情况下,延迟初始化属性的求值是同步的(synchronized):值的计算仅发生在一个线程,其他所有线程都能看到同一个值。如果委托的初始化不需要同步,可以向 lazy() 传递 LazyThreadSafetyMode.PUBLICATION 参数,这样多个线程可以同时执行。如果你能够确定初始化只会发生在单一线程,可以使用 LazyThreadSafetyMode.NONE 模式,这样不保证线程安全,也不会带来线程安全的相关开销。

Observable

  Delegates.observable() 接受两个参数:初始值和应对修改的处理程序(Handler)。每当对属性进行赋值时,(在赋值完成之后)Handler 都会被调用,Handler 有三个参数,被赋值的属性、属性的旧值和新值:

[code lang=”kotlin”]import kotlin.properties.Delegates

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

fun main(args: Array) {
val user = User()
user.name = “first”
user.name = “second”
}[/code]

上面例子的输出为:

-> first
first -> second

  如果你想要拦截对属性的赋值并“否决(Veto)”它,需要使用 vetoable() 替代 observable()。传递给 vetoable 的 Handler 会在赋值发生之前被调用。

Storing Properties in a Map

  把属性值存储在映射(Map)里也是一种常见用例,比如解析 JSON 或进行其他一些“动态”的操作,此时可以直接使用映射实例本身作为属性的委托。

[code lang=”kotlin”]class User(val map: Map) {
val name: String by map
val age: Int by map
}[/code]

在上面的例子中,构造器接受一个映射:

[code lang=”kotlin”]val user = User(mapOf(
“name” to “John Doe”,
“age” to 25
))[/code]

  委托属性的值取自映射(字符串键对应属性名称):

[code lang=”kotlin”]println(user.name) // Prints “John Doe”
println(user.age) // Prints 25[/code]

  对于 var 属性,要使用 MutableMap 替代只读的 Map

[code lang=”kotlin”]class MutableUser(val map: MutableMap) {
var name: String by map
var age: Int by map
}[/code]

Local Delegated Properties (since 1.1)

  局部变量也可以声明为委托属性,举例来说,可以延迟初始化局部变量:

[code lang=”kotlin”]fun example(computeFoo: () -> Foo) {
val memoizedFoo by lazy(computeFoo)

if (someCondition && memoizedFoo.isValid()) {
memoizedFoo.doSomething()
}
}[/code]

memoizedFoo 变量的值只会在第一次访问时计算,如果 someCondition 失败了,该变量的值就完全不会被计算。

Property Delegate Requirements

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

  对于只读属性(即 val),委托对象必须提供一个名为 getValue 的函数,该函数接受如下参数:

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

这个方法必须返回与属性相同的类型(或其子类)。

  对于可变属性(var),委托对象必须额外实现一个名为 setValue 的函数,该函数接受如下参数:

  • thisRef:与 getValue() 相同,
  • property:与 getValue() 相同。
  • 新值:类型必须与属性相同的,或其子类。

  getValue() 和/或 setValue() 可以用委托类的成员函数的形式提供,也可以用扩展函数的形式提供,后者在需要把属性委托给没有实现这些方法的对象时更加方便。这两个函数都必须使用 operator 关键字标记。

  委托类可以实现标准库中的 ReadOnlyProperty 接口或 ReadWriteProperty 接口之一,这两个接口包含了委托属性所要求的 operator 函数:

[code lang=”kotlin”]interface ReadOnlyProperty {
operator fun getValue(thisRef: R, property: KProperty<*>): T
}

interface ReadWriteProperty {
operator fun getValue(thisRef: R, property: KProperty<*>): T
operator fun setValue(thisRef: R, property: KProperty<*>, value: T)
}[/code]

Translation Rules

  在每一个委托属性背后,Kotlin 编译器都会生成一个隐藏的辅助属性并委托于它。举例来说,对于如下的属性 prop,生成的隐藏属性为 prop$delegateprop 访问器的代码只是简单地把访问委托给了 prop$delegate

[code lang=”kotlin”]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)
}[/code]

  Kotlin 编译器为 getValue()setValue() 提供了关于 prop 的全部必要信息,第一个参数 this 指外部类 C 的实例,this::propKProperty 的反射对象,描述了 prop 自身。

  注意从 Kotlin 1.1 起才支持在代码中使用如 this::prop 的语法指向 绑定可调用引用

Providing a delegate (since 1.1)

  通过定义 provideDelegate 操作符,可以对属性实现所委托的对象的创建逻辑进行扩展。如果 by 右边的对象定义了 provideDelegate 作为成员或扩展函数,则在创建属性委托实例的时候,该函数就会被调用。

  provideDelegate 的一个用例是在属性创建时检查属性的一致性,而不仅仅是在 getter 和 setter 中。

  例如,如果你想在绑定委托前检查属性名,可以这样写:

[code lang=”kotlin”]class ResourceLoader(id: ResourceID) {
operator fun provideDelegate(
thisRef: MyUI,
prop: KProperty<*>
): ReadOnlyProperty {
checkProperty(thisRef, prop.name)
// create delegate
}

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

fun bindResource(id: ResourceID): ResourceLoader { … }

class MyUI {
val image by bindResource(ResourceID.image_id)
val text by bindResource(ResourceID.text_id)
}[/code]

  provideDelegate 的参数和 getValue 相同,

  • thisRef:类型必须与属性拥有者(对于扩展属性的情况,是被扩展的类型)相同,或是其超类,
  • property:类型必须是 KProperty<*> 或其超类。

  在创建 MyUI 实例时,会为 MyUI 的每一个属性调用 provideDelegate 方法,并立即进行必要的校验。

  如果没有这种对绑定属性和委托进行拦截的机制,要实现类似的功能就必须显式地传递属性名称,并不是很方便:

[code lang=”kotlin”]// 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 MyUI.bindResource(
id: ResourceID,
propertyName: String
): ReadOnlyProperty {
checkProperty(this, propertyName)
// create delegate
}[/code]

  在自动生成的代码中,provideDelegate 被用来初始化辅助的 prop$delegate 属性,与前面的代码相比,对于同样的属性声明 val prop: Type by MyDelegate(),这里生成的代码中多出了 provideDelegate 方法:

[code lang=”kotlin”]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)
val prop: Type
get() = prop$delegate.getValue(this, this::prop)
}[/code]

  注意 provideDelegate 仅影响辅助属性的生成,而不影响为 getter 和 setter 生成的代码。