属性委托
属性可以委托其访问器逻辑。
你可以使用 by
关键字将属性连接到一个委托:
val/var property by delegate
委托的类必须包含一个 getValue()
函数,如果属性是 val
(只读),或者同时包含 getValue()
和 setValue()
函数,如果属性是 var
(读/写)。首先考虑只读的情况:
// PropertyDelegation/BasicRead.kt
package propertydelegation
import atomictest.eq
import kotlin.reflect.KProperty
class Readable(val i: Int) {
val value: String by BasicRead()
}
class BasicRead {
operator fun getValue(
r: Readable,
property: KProperty<*>
) = "getValue: ${r.i}"
}
fun main() {
val x = Readable(11)
val y = Readable(17)
x.value eq "getValue: 11"
y.value eq "getValue: 17"
}
Readable
中的 value
被委托给了一个 BasicRead
对象。getValue()
接受一个 Readable
参数,使其能够访问 Readable
对象 - 当你使用 by
时,它会将 BasicRead
绑定到整个 Readable
对象。注意,getValue()
访问了 Readable
中的 i
。
因为 getValue()
返回一个 String
,所以 value
的类型也必须是 String
。
第二个 getValue()
参数 property
是特殊类型 KProperty
,它提供了关于委托属性的反射信息。
如果委托属性是一个 var
,它必须处理读和写,因此委托类需要同时包含 getValue()
和 setValue()
:
// PropertyDelegation/BasicReadWrite.kt
package propertydelegation
import atomictest.eq
import kotlin.reflect.KProperty
class ReadWriteable(var i: Int) {
var msg = ""
var value: String by BasicReadWrite()
}
class BasicReadWrite {
operator fun getValue(
rw: ReadWriteable,
property: KProperty<*>
) = "getValue: ${rw.i}"
operator fun setValue(
rw: ReadWriteable,
property: KProperty<*>,
s: String
) {
rw.i = s.toIntOrNull() ?: 0
rw.msg = "setValue to ${rw.i}"
}
}
fun main() {
val x = ReadWriteable(11)
x.value eq "getValue: 11"
x.value = "99"
x.msg eq "setValue to 99"
x.value eq "getValue: 99"
}
前两个 setValue()
参数与 getValue()
相同,第三个是 =
右侧的值,即我们要设置的值。getValue()
和 setValue()
必须就读和写的类型达成一致,本例中为 String
(ReadWriteable
中 value
的类型)。
注意,setValue()
访问了 ReadWriteable
中的 i
和 msg
。
BasicRead.kt
和 BasicReadWrite.kt
并没有实现一个 interface
。一个类可以作为委托使用,只要它符合具有必要的函数和必要签名的约定即可。然而,你也可以像在 BasicRead2
中看到的那样实现 ReadOnlyProperty
interface
:
// PropertyDelegation/BasicRead2.kt
package propertydelegation
import atomictest.eq
import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KProperty
class Readable2(val i: Int) {
val value: String by BasicRead2()
// SAM 转换:
val value2: String by
ReadOnlyProperty { _, _ -> "getValue: $i" }
}
class BasicRead2 :
ReadOnlyProperty<Readable2, String> {
override operator fun getValue(
thisRef: Readable2,
property: KProperty<*>
) = "getValue: ${thisRef.i}"
}
fun main() {
val x = Readable2(11)
val y = Readable2(17)
x.value eq "getValue: 11"
x.value2 eq "getValue: 11"
y.value eq "getValue: 17"
y.value2 eq "getValue: 17"
}
实现 ReadOnlyProperty
向读者传达了 BasicRead2
可以作为委托使用,并确保了适当的 getValue()
定义。
由于 ReadOnlyProperty
只有一个成员函数(并且它在标准库中被定义为 fun interface
),value2
可以使用 SAM 转换 更简洁地定义。
BasicReadWrite.kt
可以修改为实现 ReadWriteProperty
,以确保适当的 getValue()
和 setValue()
定义:
// PropertyDelegation/BasicReadWrite2.kt
package propertydelegation
import atomictest.eq
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
class ReadWriteable2(var i: Int) {
var msg = ""
var value: String by BasicReadWrite2()
}
class BasicReadWrite2 :
ReadWriteProperty<ReadWriteable2, String> {
override operator fun getValue(
rw: ReadWriteable2,
property: KProperty<*>
) = "getValue: ${rw.i}"
override operator fun setValue(
rw: ReadWriteable2,
property: KProperty<*>,
s: String
) {
rw.i = s.toIntOrNull() ?: 0
rw.msg = "setValue to ${rw.i}"
}
}
fun main() {
val x = ReadWriteable2(11)
x.value eq "getValue: 11"
x.value = "99"
x.msg eq "setValue to 99"
x.value eq "getValue: 99"
}
因此,委托类必须包含以下函数中的一个或两个,当访问委托属性时将调用这些函数:
-
用于读取:
operator fun getValue(thisRef: T, property: KProperty<*>): V
-
用于写入:
setValue(thisRef: T, property: KProperty<*>, value: V)
如果委托属性是 val
,则只需要第一个函数,可以使用 [SAM 转换](se
05-ch01.md#sam-转换) 实现 ReadOnlyProperty
。
参数是:
thisRef: T
指向委托对象,其中T
是该委托的类型。如果你在函数中不想使用thisRef
,你可以使用Any?
作为T
来有效地禁用它。property: KProperty<*>
提供有关属性本身的信息。最常用的是name
,它产生委托属性的字段名。value
是setValue()
存储到委托属性中的值。V
是该属性的类型。
getValue()
和 setValue()
可以按照约定进行定义,也可以作为 ReadOnlyProperty
或 ReadWriteProperty
的实现编写。
要启用对 private
元素的访问,嵌套委托类:
// PropertyDelegation/Accessibility.kt
package propertydelegation
import atomictest.eq
import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KProperty
class Person(
private val first: String,
private val last: String
) {
val name by // SAM 转换:
ReadOnlyProperty<Person, String> { _, _ ->
"$first $last"
}
}
fun main() {
val alien = Person("Floopy", "Noopers")
alien.name eq "Floopy Noopers"
}
假设在委托类中有足够的访问权限,getValue()
和 setValue()
可以编写为扩展函数:
// PropertyDelegation/Add.kt
package propertydelegation2
import atomictest.eq
import kotlin.reflect.KProperty
class Add(val a: Int, val b: Int) {
val sum by Sum()
}
class Sum
operator fun Sum.getValue(
thisRef: Add,
property: KProperty<*>
) = thisRef.a + thisRef.b
fun main() {
val addition = Add(144, 12)
addition.sum eq 156
}
这样,您可以使用一个无法修改或继承的现有类,并仍然使用它来委托属性。
在这里,当您设置属性的值时,存储的数字是该值的斐波那契数,使用来自 Recursion 一节的 fibonacci()
函数:
// PropertyDelegation/FibonacciProperty.kt
package propertydelegation
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
import recursion.fibonacci
import atomictest.eq
class Fibonacci :
ReadWriteProperty<Any?, Long> {
private var current: Long = 0
override operator fun getValue(
thisRef: Any?,
property: KProperty<*>
) = current
override operator fun setValue(
thisRef: Any?,
property: KProperty<*>,
value: Long
) {
current = fibonacci(value.toInt())
}
}
fun main() {
var fib by Fibonacci()
fib eq 0L
fib = 22L
fib eq 17711L
fib = 90L
fib eq 2880067194370816120L
}
main()
中的 fib
是一个 本地委托属性 - 它是在函数中而不是类中定义的。委托属性也可以在文件范围内定义。
ReadWriteProperty
的第一个泛型参数可以是 Any?
,因为我们在 Fibonacci
内部不使用它来访问任何东西,这需要特定的类型信息。相反,我们在任何成员函数中一样操作 current
属性。
在我们迄今为止看到的大多数示例中,getValue()
和 setValue()
的第一个参数都是特定类型的。那些委托与该特定类型绑定。有时,可以通过将第一个类型忽略为 Any?
来创建一个通用的委托。例如,假设我们希望将每个委托的 String
属性存储在以该属性命名的文本文件中:
// PropertyDelegation/FileDelegate.kt
package propertydelegation
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
import checkinstructions.DataFile
class FileDelegate :
ReadWriteProperty<Any?, String> {
override fun getValue(
thisRef: Any?,
property: KProperty<*>
): String {
val file =
DataFile(property.name + ".txt")
return if (file.exists())
file.readText()
else ""
}
override fun setValue(
thisRef: Any?,
property: KProperty<*>,
value: String
) {
DataFile(property.name + ".txt")
.writeText(value)
}
}
这个委托只需要与文件交互,并且不需要通过 thisRef
访问任何东西。我们通过将 thisRef
类型定义为 Any?
来忽略 thisRef
,因为 Any?
没有有趣的操作。我们对 property.name
感兴趣,它是字段的名称。现在,我们可以自动创建与每个属性关联的文件,并将该属性的数据存储在该文件中:
// PropertyDelegation/Configuration.kt
package propertydelegation
import checkinstructions.DataFile
import atomictest.eq
class Configuration {
var user by FileDelegate()
var id by FileDelegate()
var project by FileDelegate()
}
fun main() {
val config = Configuration()
config.user = "Luciano"
config.id = "Ramalho47"
config.project = "MyLittlePython"
DataFile("user.txt").readText() eq "Luciano"
DataFile("id.txt").readText() eq "Ramalho47"
DataFile("project.txt").readText() eq
"MyLittlePython"
}
因为它可以忽略周围的类型,所以 FileDelegate
是可重用的。
练习和解答可在 www.AtomicKotlin.com 找到。