Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

属性委托

属性可以委托其访问器逻辑。

你可以使用 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() 必须就读和写的类型达成一致,本例中为 StringReadWriteablevalue 的类型)。

注意,setValue() 访问了 ReadWriteable 中的 imsg

BasicRead.ktBasicReadWrite.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"
}

因此,委托类必须包含以下函数中的一个或两个,当访问委托属性时将调用这些函数:

  1. 用于读取:

    operator fun getValue(thisRef: T, property: KProperty<*>): V

  2. 用于写入:

    setValue(thisRef: T, property: KProperty<*>, value: V)

如果委托属性是 val,则只需要第一个函数,可以使用 [SAM 转换](se

05-ch01.md#sam-转换) 实现 ReadOnlyProperty

参数是:

  • thisRef: T 指向委托对象,其中 T 是该委托的类型。如果你在函数中不想使用 thisRef,你可以使用 Any? 作为 T 来有效地禁用它。
  • property: KProperty<*> 提供有关属性本身的信息。最常用的是 name,它产生委托属性的字段名。
  • valuesetValue() 存储到委托属性中的值。V 是该属性的类型。

getValue()setValue() 可以按照约定进行定义,也可以作为 ReadOnlyPropertyReadWriteProperty 的实现编写。

要启用对 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 找到。