属性委托工具
标准库包含特殊的属性委托操作。
Map
是 Kotlin 标准库中少数几种预配置用作委托属性的类型之一。一个 Map
可以用于存储类中的所有属性。每个属性标识符都成为映射的 String
键,属性的类型则在关联的值中捕获:
// DelegationTools/CarService.kt
package propertydelegation
import atomictest.eq
class Driver(
map: MutableMap<String, Any?>
) {
var name: String by map
var age: Int by map
var id: String by map
var available: Boolean by map
var coord: Pair<Double, Double> by map
}
fun main() {
val info = mutableMapOf<String, Any?>(
"name" to "Bruno Fiat",
"age" to 22,
"id" to "X97C111",
"available" to false,
"coord" to Pair(111.93, 1231.12)
)
val driver = Driver(info)
driver.available eq false
driver.available = true
info eq "{name=Bruno Fiat, age=22, " +
"id=X97C111, available=true, " +
"coord=(111.93, 1231.12)}"
}
注意,当设置 driver.available = true
时,原始的 Map
info
被修改。这是因为 Kotlin 标准库包含了 Map
扩展函数 getValue()
和 setValue()
,它们使得属性委托成为可能。以下是它们的简化版本,展示了它们的工作原理:
// DelegationTools/MapAccessors.kt
package delegationtools
import kotlin.reflect.KProperty
operator fun MutableMap<String, Any>.getValue(
thisRef: Any?, property: KProperty<*>
): Any? {
return this[property.name]
}
operator fun MutableMap<String, Any>.setValue(
thisRef: Any?, property: KProperty<*>,
value: Any
) {
this[property.name] = value
}
要查看实际的库定义,在 IntelliJ IDEA 或 Android Studio 中将光标放在 by
关键字上,然后调用 “转到声明”。
Delegates.observable()
可以观察可变属性的修改。在这里,我们追踪旧值和新值:
// DelegationTools/Team.kt
package delegationtools
import kotlin.properties.Delegates.observable
import atomictest.eq
class Team {
var msg = ""
var captain: String by observable("<0>") {
prop, old, new ->
msg += "${prop.name} $old to $new "
}
}
fun main() {
val team = Team()
team.captain = "Adam"
team.captain = "Amanda"
team.msg eq "captain <0> to Adam " +
"captain Adam to Amanda"
}
observable()
接受两个参数:
- 属性的初始值;在这里是
"<0>"
。 - 一个函数,当属性被修改时要执行的操作。在这里,我们使用了一个 lambda。函数的参数是正在更改的属性、该属性当前的值以及正在更改为的值。
Delegates.vetoable()
允许您在新属性值不满足给定谓词时阻止对属性的更改。在这里,aName()
坚持要求团队队长的名字以字母 “A” 开头:
// DelegationTools/TeamWithTraditions.kt
package delegationtools
import atomictest.*
import kotlin.properties.Delegates
import kotlin.reflect.KProperty
fun aName(
property: KProperty<*>,
old: String,
new: String
) = if (new.startsWith("A")) {
trace("$old -> $new")
true
} else {
trace("Name must start with 'A'")
false
}
interface Captain {
var captain: String
}
class TeamWithTraditions : Captain {
override var captain: String
by Delegates.vetoable("Adam", ::aName)
}
class TeamWithTraditions2 : Captain {
override var captain: String
by Delegates.vetoable("Adam") {
_, old, new ->
if (new.startsWith("A")) {
trace("$old -> $new")
true
} else {
trace("Name must start with 'A'")
false
}
}
}
fun main() {
listOf(
TeamWithTraditions(),
TeamWithTraditions2()
).forEach {
it.captain = "Amanda"
it.captain = "Bill"
it.captain eq "Amanda"
}
trace eq """
Adam -> Amanda
Name must start with 'A'
Adam -> Amanda
Name must start with 'A'
"""
}
Delegates.vetoable()
接受两个参数:属性的初始值,以及一个 onChange()
函数,这在这个例子中是 ::aName
。onChange()
接受三个参数:property: KProperty<*>
,属性当前持有的 old
值,以及放置在属性中的 new
值。该函数返回一个 Boolean
,指示更改是否成功或被阻止。
TeamWithTraditions2
使用了一个 lambda 定义了 Delegates.vetoable()
,而不是函数 aName()
。
properties.Delegates
中剩余的工具是 notNull()
,它生成一个必须在读取之前初始化的属性:
// DelegationTools/NeverNull.kt
package delegationtools
import atomictest.*
import kotlin.properties.Delegates
class NeverNull {
var nn: Int by Delegates.notNull()
}
fun main() {
val non = NeverNull()
capture {
non.nn
} eq "IllegalStateException: Property " +
"nn should be initialized before get."
non.nn = 11
non.nn eq 11
}
在为 nn
分配值之前尝试读取 non.nn
会产生异常。在为 nn
分配值之后,可以成功读取它。
练习和解答可在 www.AtomicKotlin.com 找到。