Eigenschaftszugriffe

Um eine Eigenschaft zu lesen, verwenden Sie ihren Namen. Um einer veränderlichen Eigenschaft einen Wert zuzuweisen, verwenden Sie den Zuweisungsoperator =.

Dies liest und schreibt die Eigenschaft i:

// PropertyAccessors/Data.kt
package propertyaccessors
import atomictest.eq

class Data(var i: Int)

fun main() {
  val data = Data(10)
  data.i eq 10 // Read the 'i' property
  data.i = 20  // Write to the 'i' property
}

Dies scheint ein direkter Zugriff auf das Speicherelement namens i zu sein. Allerdings ruft Kotlin Funktionen auf, um die Lese- und Schreiboperationen durchzuführen. Wie erwartet, lesen und schreiben diese Funktionen standardmäßig die in i gespeicherten Daten. In diesem Abschnitt lernen Sie, Ihre eigenen Eigenschaftszugriffe zu schreiben, um die Lese- und Schreibaktionen anzupassen.

Der Zugriff, der verwendet wird, um den Wert einer Eigenschaft zu erhalten, wird Getter genannt. Sie erstellen einen Getter, indem Sie get() direkt nach der Eigenschaftsdefinition definieren. Der Zugriff, der verwendet wird, um eine änderbare Eigenschaft zu modifizieren, wird Setter genannt. Sie erstellen einen Setter, indem Sie set() direkt nach der Eigenschaftsdefinition definieren.

Die in dem folgenden Beispiel definierten Eigenschaftszugriffe imitieren die von Kotlin generierten Standardimplementierungen. Wir zeigen zusätzliche Informationen an, damit Sie sehen können, dass die Eigenschaftszugriffe tatsächlich während der Lese- und Schreibvorgänge aufgerufen werden. Wir rücken get() und set() ein, um sie visuell mit der Eigenschaft zu verknüpfen, aber die eigentliche Verknüpfung erfolgt, weil get() und set() direkt nach dieser Eigenschaft definiert sind (Kotlin kümmert sich nicht um die Einrückung):

// PropertyAccessors/Default.kt
package propertyaccessors
import atomictest.*

class Default {
  var i: Int = 0
    get() {
      trace("get()")
      return field       // [1]
    }
    set(value) {
      trace("set($value)")
      field = value      // [2]
    }
}

fun main() {
  val d = Default()
  d.i = 2
  trace(d.i)
  trace eq """
    set(2)
    get()
    2
  """
}

Die Reihenfolge der Definition von get() und set() ist unwichtig. Sie können get() definieren, ohne set() zu definieren, und umgekehrt.

Das Standardverhalten einer Eigenschaft gibt ihren gespeicherten Wert über einen Getter zurück und modifiziert ihn mit einem Setter—die Aktionen von [1] und [2]. Innerhalb des Getters und Setters wird der gespeicherte Wert indirekt mit dem Schlüsselwort field manipuliert, das nur innerhalb dieser beiden Funktionen zugänglich ist.

Das nächste Beispiel verwendet die Standardimplementierung des Getters und fügt einen Setter hinzu, um Änderungen an der Eigenschaft n nachzuverfolgen:

// PropertyAccessors/LogChanges.kt
package propertyaccessors
import atomictest.*

class LogChanges {
  var n: Int = 0
    set(value) {
      trace("$field becomes $value")
      field = value
    }
}

fun main() {
  val lc = LogChanges()
  lc.n eq 0
  lc.n = 2
  lc.n eq 2
  trace eq "0 becomes 2"
}

Wenn Sie eine Eigenschaft als private definieren, werden beide Zugriffsmethoden private. Sie können auch den Setter private machen und den Getter public. Dann können Sie die Eigenschaft außerhalb der Klasse lesen, aber ihren Wert nur innerhalb der Klasse ändern:

// PropertyAccessors/Counter.kt
package propertyaccessors
import atomictest.eq

class Counter {
  var value: Int = 0
    private set
  fun inc() = value++
}

fun main() {
  val counter = Counter()
  repeat(10) {
    counter.inc()
  }
  counter.value eq 10
}

Mit private set kontrollieren wir die Eigenschaft value, sodass sie nur um eins erhöht werden kann.

Normale Eigenschaften speichern ihre Daten in einem Feld. Man kann auch eine Eigenschaft erstellen, die kein Feld hat:

// PropertyAccessors/Hamsters.kt
package propertyaccessors
import atomictest.eq

class Hamster(val name: String)

class Cage(private val maxCapacity: Int) {
  private val hamsters =
    mutableListOf<Hamster>()
  val capacity: Int
    get() = maxCapacity - hamsters.size
  val full: Boolean
    get() = hamsters.size == maxCapacity
  fun put(hamster: Hamster): Boolean =
    if (full)
      false
    else {
      hamsters += hamster
      true
    }
  fun take(): Hamster =
    hamsters.removeAt(0)
}

fun main() {
  val cage = Cage(2)
  cage.full eq false
  cage.capacity eq 2
  cage.put(Hamster("Alice")) eq true
  cage.put(Hamster("Bob")) eq true
  cage.full eq true
  cage.capacity eq 0
  cage.put(Hamster("Charlie")) eq false
  cage.take()
  cage.capacity eq 1
}

Die Eigenschaften capacity und full enthalten keinen zugrunde liegenden Zustand—sie werden zum Zeitpunkt jedes Zugriffs berechnet. Sowohl capacity als auch full sind ähnlich wie Funktionen, und Sie können sie als solche definieren:

// PropertyAccessors/Hamsters2.kt
package propertyaccessors

class Cage2(private val maxCapacity: Int) {
  private val hamsters =
    mutableListOf<Hamster>()
  fun capacity(): Int =
    maxCapacity - hamsters.size
  fun isFull(): Boolean =
    hamsters.size == maxCapacity
}

In diesem Fall verbessert die Verwendung von Eigenschaften die Lesbarkeit, da Kapazität und Fülle Eigenschaften des Käfigs sind. Wandeln Sie jedoch nicht einfach alle Ihre Funktionen in Eigenschaften um—sehen Sie sich zuerst an, wie sie sich lesen.

  • -

Die Kotlin-Stilrichtlinie bevorzugt Eigenschaften gegenüber Funktionen, wenn der Wert günstig zu berechnen ist und die Eigenschaft bei jedem Aufruf dasselbe Ergebnis liefert, solange sich der Objektzustand nicht geändert hat.

Eigenschaftszugriffe bieten eine Art Schutz für Eigenschaften. Viele objektorientierte Sprachen verlassen sich darauf, ein physisches Feld private zu machen, um den Zugriff auf diese Eigenschaft zu kontrollieren. Mit Eigenschaftszugriffen können Sie Code hinzufügen, um diesen Zugriff zu kontrollieren oder zu verändern, während Sie jedem erlauben, eine Eigenschaft zu verwenden.

Übungen und Lösungen finden Sie auf www.AtomicKotlin.com.