Abschnitt II: Einführung in Objekte

Objekte sind die Grundlage für zahlreiche moderne Sprachen, einschließlich Kotlin.

In einer objektorientierten (OO) Programmiersprache entdecken Sie “Nomen” in dem Problem, das Sie lösen, und übersetzen diese Nomen in Objekte. Objekte halten Daten und führen Aktionen aus. Eine objektorientierte Sprache erstellt und verwendet Objekte.

Kotlin ist nicht nur objektorientiert; es ist auch funktional. Funktionale Sprachen konzentrieren sich auf die Aktionen, die Sie ausführen (“Verben”). Kotlin ist eine hybride objekt-funktionale Sprache.

Objekte überall

Objekte speichern Daten mithilfe von Eigenschaften (vals und vars) und führen Operationen mit diesen Daten mithilfe von Funktionen durch.

Einige Definitionen:

  • Klasse: Definiert Eigenschaften und Funktionen für das, was im Wesentlichen ein neuer Datentyp ist. Klassen werden auch als benutzerdefinierte Typen bezeichnet.
  • Mitglied: Entweder eine Eigenschaft oder eine Funktion einer Klasse.
  • Mitgliedsfunktion: Eine Funktion, die nur mit einer bestimmten Klasse von Objekten arbeitet.
  • Ein Objekt erstellen: Ein val oder var einer Klasse erstellen. Auch als eine Instanz dieser Klasse erstellen bezeichnet.

Da Klassen Zustand und Verhalten definieren, können wir sogar Instanzen von eingebauten Typen wie Double oder Boolean als Objekte bezeichnen.

Betrachten Sie die IntRange-Klasse von Kotlin:

// ObjectsEverywhere/IntRanges.kt

fun main() {
  val r1 = IntRange(0, 10)
  val r2 = IntRange(5, 7)
  println(r1)
  println(r2)
}
/* Output:
0..10
5..7
*/

Wir erstellen zwei Objekte (Instanzen) der Klasse IntRange. Jedes Objekt hat seinen eigenen Speicherplatz im Speicher. IntRange ist eine Klasse, aber ein bestimmter Bereich r1 von 0 bis 10 ist ein Objekt, das sich von dem Bereich r2 unterscheidet.

Für ein IntRange-Objekt stehen zahlreiche Operationen zur Verfügung. Einige sind einfach, wie sum(), und andere erfordern mehr Verständnis, bevor Sie sie verwenden können. Wenn Sie versuchen, eine aufzurufen, die Argumente benötigt, wird die IDE nach diesen Argumenten fragen.

Um mehr über eine bestimmte Mitgliedsfunktion zu erfahren, schlagen Sie in der Kotlin-Dokumentation nach. Beachten Sie das Lupensymbol im oberen rechten Bereich der Seite. Klicken Sie darauf und geben Sie IntRange in das Suchfeld ein. Klicken Sie auf kotlin.ranges > IntRange aus der resultierenden Suche. Sie sehen die Dokumentation für die IntRange-Klasse. Sie können alle Mitgliedsfunktionen—die Programmierschnittstelle (API)—der Klasse studieren. Obwohl Sie die meisten davon zu diesem Zeitpunkt nicht verstehen werden, ist es hilfreich, sich daran zu gewöhnen, in der Kotlin-Dokumentation nachzuschlagen.

Ein IntRange ist eine Art von Objekt, und ein charakteristisches Merkmal eines Objekts ist, dass Sie Operationen darauf ausführen. Statt “eine Operation ausführen” sagen wir eine Mitgliedsfunktion aufrufen. Um eine Mitgliedsfunktion für ein Objekt aufzurufen, beginnen Sie mit dem Objektbezeichner, dann ein Punkt, dann der Name der Operation:

// ObjectsEverywhere/RangeSum.kt

fun main() {
  val r = IntRange(0, 10)
  println(r.sum())
}
/* Output:
55
*/

Da sum() eine Mitgliedsfunktion ist, die für IntRange definiert ist, rufen Sie sie auf, indem Sie r.sum() schreiben. Dies summiert alle Zahlen in diesem IntRange.

Frühere objektorientierte Sprachen verwendeten den Ausdruck “eine Nachricht senden”, um das Aufrufen einer Mitgliedsfunktion für ein Objekt zu beschreiben. Manchmal sieht man diese Terminologie noch.

Klassen können viele Operationen (Mitgliedsfunktionen) haben. Es ist einfach, Klassen mit einer IDE (integrierte Entwicklungsumgebung) zu erkunden, die eine Funktion namens Code-Vervollständigung enthält. Wenn Sie zum Beispiel .s nach einem Objektbezeichner in IntelliJ IDEA eingeben, zeigt es alle Mitglieder dieses Objekts an, die mit s beginnen:

Code-Vervollständigung
Code-Vervollständigung

Versuchen Sie, die Code-Vervollständigung bei anderen Objekten zu verwenden. Zum Beispiel können Sie einen String umkehren oder alle Zeichen in Kleinbuchstaben umwandeln:

// ObjectsEverywhere/Strings.kt

fun main() {
  val s = "AbcD"
  println(s.reversed())
  println(s.lowercase())
}
/* Output:
DcbA
abcd
*/

Du kannst einen String leicht in einen integer umwandeln und zurück:

// ObjectsEverywhere/Conversion.kt

fun main() {
  val s = "123"
  println(s.toInt())
  val i = 123
  println(i.toString())
}
/* Output:
123
123
*/

Später im Buch besprechen wir Strategien, um Situationen zu bewältigen, wenn der String, den Sie konvertieren möchten, keinen korrekten Integer-Wert darstellt.

Sie können auch von einem Zahlentyp zu einem anderen konvertieren. Um Verwirrung zu vermeiden, sind Konvertierungen zwischen Zahlentypen explizit. Zum Beispiel konvertieren Sie ein Int i zu einem Long, indem Sie i.toLong() aufrufen, oder zu einem Double mit i.toDouble():

// ObjectsEverywhere/NumberConversions.kt

fun fraction(numerator: Long, denom: Long) =
  numerator.toDouble() / denom

fun main() {
  val num = 1
  val den = 2
  val f = fraction(num.toLong(), den.toLong())
  println(f)
}
/* Output:
0.5
*/

Gut definierte Klassen sind für einen Programmierer leicht zu verstehen und erzeugen Code, der leicht zu lesen ist.

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

Klassen erstellen

Sie können nicht nur vordefinierte Typen wie IntRange und String verwenden, sondern auch Ihre eigenen Objekttypen erstellen.

Tatsächlich besteht ein Großteil der Aktivitäten in der objektorientierten Programmierung darin, neue Typen zu erstellen. Sie erstellen neue Typen, indem Sie Klassen definieren.

Ein Objekt ist ein Teil der Lösung für ein Problem, das Sie zu lösen versuchen. Beginnen Sie damit, Objekte als Ausdruck von Konzepten zu betrachten. Als erste Annäherung, wenn Sie ein “Ding” in Ihrem Problem entdecken, repräsentieren Sie dieses Ding als Objekt in Ihrer Lösung.

Angenommen, Sie möchten ein Programm erstellen, um Tiere in einem Zoo zu verwalten. Es ist sinnvoll, die verschiedenen Tierarten basierend darauf zu kategorisieren, wie sie sich verhalten, welche Bedürfnisse sie haben, mit welchen Tieren sie sich vertragen und mit welchen sie kämpfen. Alles, was eine Tierart unterscheidet, wird in der Klassifizierung des Objekts dieses Tieres erfasst. Kotlin verwendet das Schlüsselwort class, um einen neuen Objekttyp zu erstellen:

// CreatingClasses/Animals.kt

// Create some classes:
class Giraffe
class Bear
class Hippo

fun main() {
  // Create some objects:
  val g1 = Giraffe()
  val g2 = Giraffe()
  val b = Bear()
  val h = Hippo()

  // Each object() is unique:
  println(g1)
  println(g2)
  println(h)
  println(b)
}
/* Sample output:
Giraffe@28d93b30
Giraffe@1b6d3586
Hippo@4554617c
Bear@74a14482
*/

Um eine Klasse zu definieren, beginnen Sie mit dem Schlüsselwort class, gefolgt von einem Bezeichner für Ihre neue Klasse. Der Klassenname muss mit einem Buchstaben (A-Z, Groß- oder Kleinbuchstaben) beginnen, kann jedoch Zahlen und Unterstriche enthalten. Nach Konvention wird der erste Buchstabe eines Klassennamens großgeschrieben, während der erste Buchstabe aller vals und vars kleingeschrieben wird.

Animals.kt beginnt mit der Definition von drei neuen Klassen und erstellt dann vier Objekte (auch Instanzen genannt) dieser Klassen.

Giraffe ist eine Klasse, aber eine bestimmte fünfjährige männliche Giraffe, die in Botswana lebt, ist ein Objekt. Jedes Objekt unterscheidet sich von allen anderen, daher geben wir ihnen Namen wie g1 und g2.

Beachten Sie die etwas kryptische Ausgabe der letzten vier Zeilen. Der Teil vor dem @ ist der Klassenname und die Zahl nach dem @ ist die Adresse, an der sich das Objekt im Speicher Ihres Computers befindet. Ja, das ist eine Zahl, auch wenn sie einige Buchstaben enthält - das nennt man “hexadezimale Notation”. Jedes Objekt in Ihrem Programm hat seine eigene eindeutige Adresse.

Die hier definierten Klassen (Giraffe, Bear und Hippo) sind so einfach wie möglich: die gesamte Klassendefinition besteht aus einer einzigen Zeile. Komplexere Klassen verwenden geschweifte Klammern ({ und }), um einen Klassenkörper zu erstellen, der die Merkmale und Verhaltensweisen dieser Klasse enthält.

Eine innerhalb einer Klasse definierte Funktion gehört zu dieser Klasse. In Kotlin nennen wir sie Mitgliedsfunktionen der Klasse. Einige objektorientierte Programmiersprachen wie Java entscheiden sich dafür, sie Methoden zu nennen, ein Begriff, der aus frühen objektorientierten Sprachen wie Smalltalk stammt. Um die funktionale Natur von Kotlin zu betonen, entschieden sich die Designer, den Begriff Methode wegzulassen, da einige Anfänger die Unterscheidung verwirrend fanden. Stattdessen wird in der gesamten Sprache der Begriff Funktion verwendet.

Wenn es eindeutig ist, sagen wir einfach “Funktion”. Wenn wir die Unterscheidung treffen müssen:

  • Mitgliedsfunktionen gehören zu einer Klasse.
  • Top-Level-Funktionen existieren für sich und sind nicht Teil einer Klasse.

Hier gehört bark() zur Dog-Klasse:

// CreatingClasses/Dog.kt

class Dog {
  fun bark() = "yip!"
}

fun main() {
  val dog = Dog()
}

In main() erstellen wir ein Dog-Objekt und weisen es val dog zu. Kotlin gibt eine Warnung aus, weil wir dog nie verwenden.

Mitgliedsfunktionen werden aufgerufen (invoked), indem man den Objektnamen verwendet, gefolgt von einem . (Punkt), gefolgt vom Funktionsnamen und der Parameterliste. Hier rufen wir die Funktion meow() auf und zeigen das Ergebnis an:

// CreatingClasses/Cat.kt

class Cat {
  fun meow() = "mrrrow!"
}

fun main() {
  val cat = Cat()
  // Call 'meow()' for 'cat':
  val m1 = cat.meow()
  println(m1)
}
/* Output:
mrrrow!
*/

Eine Mitgliedsfunktion wirkt auf eine bestimmte Instanz einer Klasse. Wenn Sie meow() aufrufen, müssen Sie es mit einem Objekt aufrufen. Während des Aufrufs kann meow() auf andere Mitglieder dieses Objekts zugreifen.

Beim Aufrufen einer Mitgliedsfunktion verfolgt Kotlin das betreffende Objekt, indem es leise eine Referenz auf dieses Objekt übergibt. Diese Referenz ist innerhalb der Mitgliedsfunktion mit dem Schlüsselwort this verfügbar.

Mitgliedsfunktionen haben einen speziellen Zugriff auf andere Elemente innerhalb einer Klasse, indem sie einfach diese Elemente benennen. Sie können den Zugriff auf diese Elemente auch explizit mit this qualifizieren. Hier ruft exercise() speak() mit und ohne Qualifizierung auf:

// CreatingClasses/Hamster.kt

class Hamster {
  fun speak() = "Squeak! "
  fun exercise() =
    this.speak() +   // Qualified with 'this'
      speak() +      // Without 'this'
      "Running on wheel"
}

fun main() {
  val hamster = Hamster()
  println(hamster.exercise())
}
/* Output:
Squeak! Squeak! Running on wheel
*/

In exercise(), rufen wir zuerst speak() mit einem expliziten this auf und lassen dann die Qualifikation weg.

Manchmal sieht man Code, der ein unnötiges explizites this enthält. Solcher Code stammt oft von Programmierern, die eine andere Sprache kennen, in der this entweder erforderlich ist oder Teil des Stils ist. Die unnötige Verwendung eines Features ist verwirrend für den Leser, der Zeit damit verbringt, herauszufinden, warum Sie es tun. Wir empfehlen, die unnötige Verwendung von this zu vermeiden.

Außerhalb der Klasse muss man hamster.exercise() und hamster.speak() sagen.

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

Eigenschaften

Eine Eigenschaft ist ein var oder val, das Teil einer Klasse ist.

Das Definieren einer Eigenschaft beibehält den Zustand innerhalb einer Klasse. Das Beibehalten des Zustands ist der Hauptmotivationsgrund, eine Klasse zu erstellen, anstatt nur eine oder mehrere eigenständige Funktionen zu schreiben.

Eine var-Eigenschaft kann neu zugewiesen werden, während eine val-Eigenschaft dies nicht kann. Jedes Objekt erhält seinen eigenen Speicher für Eigenschaften:

// Properties/Cup.kt

class Cup {
  var percentFull = 0
}

fun main() {
  val c1 = Cup()
  c1.percentFull = 50
  val c2 = Cup()
  c2.percentFull = 100

  println(c1.percentFull)
  println(c2.percentFull)
}
/* Output:
50
100
*/

Das Definieren eines var oder val innerhalb einer Klasse sieht genauso aus wie das Definieren innerhalb einer Funktion. Allerdings wird das var oder val Teil dieser Klasse, und Sie müssen darauf verweisen, indem Sie das Objekt mit Punktnotation angeben, wobei Sie einen Punkt zwischen das Objekt und den Namen der Eigenschaft setzen. Sie können die Punktnotation bei jedem Verweis auf percentFull sehen.

Die Eigenschaft percentFull repräsentiert den Zustand des entsprechenden Cup-Objekts. c1.percentFull und c2.percentFull enthalten unterschiedliche Werte, was zeigt, dass jedes Objekt über seinen eigenen Speicherplatz verfügt.

Eine Mitgliedsfunktion kann auf eine Eigenschaft innerhalb ihres Objekts verweisen, ohne die Punktnotation zu verwenden (das heißt, ohne sie zu qualifizieren):

// Properties/Cup2.kt

class Cup2 {
  var percentFull = 0
  val max = 100
  fun add(increase: Int): Int {
    percentFull += increase
    if (percentFull > max)
      percentFull = max
    return percentFull
  }
}

fun main() {
  val cup = Cup2()
  cup.add(50)
  println(cup.percentFull)
  cup.add(70)
  println(cup.percentFull)
}
/* Output:
50
100
*/

Die add()-Mitgliedsfunktion versucht, increase zu percentFull hinzuzufügen, stellt jedoch sicher, dass es nicht über 100 % hinausgeht.

Eigenschaften und Mitgliedsfunktionen müssen von außerhalb einer Klasse qualifiziert werden.

Man kann Eigenschaften auf oberster Ebene definieren:

// Properties/TopLevelProperty.kt

val constant = 42

var counter = 0

fun inc() {
  counter++
}

Die Definition eines val auf oberster Ebene ist sicher, da es nicht verändert werden kann. Die Definition einer veränderbaren (var) Eigenschaft auf oberster Ebene wird jedoch als Antimuster angesehen. Wenn Ihr Programm komplizierter wird, wird es schwieriger, den gemeinsamen veränderbaren Zustand korrekt zu verstehen. Wenn jeder in Ihrem Code Zugriff auf den var Zähler hat, können Sie nicht garantieren, dass er korrekt verändert wird: Während inc() den Zähler um eins erhöht, könnte ein anderer Teil des Programms den Zähler um zehn verringern, was zu schwer nachvollziehbaren Fehlern führt. Es ist am besten, veränderbaren Zustand innerhalb einer Klasse zu schützen. In Sichtbarkeit einschränken wird gezeigt, wie man ihn wirklich verstecken kann.

Zu sagen, dass vars verändert werden können, während vals dies nicht können, ist eine Vereinfachung. Als Analogie können Sie ein Haus als val betrachten und ein Sofa im Haus als var. Sie können das Sofa verändern, weil es ein var ist. Sie können jedoch das Haus nicht neu zuweisen, da es ein val ist:

// Properties/ChangingAVal.kt

class House {
  var sofa: String = ""
}

fun main() {
  val house = House()
  house.sofa = "Simple sleeper sofa: $89.00"
  println(house.sofa)
  house.sofa = "New leather sofa: $3,099.00"
  println(house.sofa)
  // Cannot reassign the val to a new House:
  // house = House()
}
/* Output:
Simple sleeper sofa: $89.00
New leather sofa: $3,099.00
*/

Obwohl house ein val ist, kann sein Objekt modifiziert werden, weil sofa in class House ein var ist. Die Definition von house als val verhindert nur, dass es einem neuen Objekt neu zugewiesen wird.

Wenn wir eine Eigenschaft als val definieren, kann sie nicht neu zugewiesen werden:

// Properties/AnUnchangingVar.kt

class Sofa {
  val cover: String = "Loveseat cover"
}

fun main() {
  var sofa = Sofa()
  // Not allowed:
  // sofa.cover = "New cover"
  // Reassigning a var:
  sofa = Sofa()
}

Auch wenn sofa eine var ist, kann sein Objekt nicht modifiziert werden, weil cover in class Sofa ein val ist. sofa kann jedoch einem neuen Objekt zugewiesen werden.

Wir haben über Bezeichner wie house und sofa gesprochen, als wären sie Objekte. Tatsächlich sind sie Referenzen auf Objekte. Eine Möglichkeit, dies zu sehen, ist zu beobachten, dass zwei Bezeichner auf dasselbe Objekt verweisen können:

// Properties/References.kt

class Kitchen {
  var table: String = "Round table"
}

fun main() {
  val kitchen1 = Kitchen()
  val kitchen2 = kitchen1
  println("kitchen1: ${kitchen1.table}")
  println("kitchen2: ${kitchen2.table}")
  kitchen1.table = "Square table"
  println("kitchen1: ${kitchen1.table}")
  println("kitchen2: ${kitchen2.table}")
}
/* Output:
kitchen1: Round table
kitchen2: Round table
kitchen1: Square table
kitchen2: Square table
*/

Wenn kitchen1 table verändert, sieht kitchen2 die Änderung. kitchen1.table und kitchen2.table zeigen die gleiche Ausgabe.

Denken Sie daran, dass var und val Referenzen anstelle von Objekten steuern. Ein var ermöglicht es Ihnen, eine Referenz auf ein anderes Objekt neu zu binden, während ein val dies verhindert.

Veränderlichkeit bedeutet, dass ein Objekt seinen Zustand ändern kann. In den obigen Beispielen definieren class House und class Kitchen veränderliche Objekte, während class Sofa unveränderliche Objekte definiert.

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

Konstruktoren

Sie initialisieren ein neues Objekt, indem Sie Informationen an einen Konstruktor übergeben.

Jedes Objekt ist eine isolierte Welt. Ein Programm ist eine Sammlung von Objekten, daher löst die korrekte Initialisierung jedes einzelnen Objekts einen großen Teil des Initialisierungsproblems. Kotlin enthält Mechanismen, um eine ordnungsgemäße Initialisierung von Objekten zu gewährleisten.

Ein Konstruktor ist wie eine spezielle Mitgliedsfunktion, die ein neues Objekt initialisiert. Die einfachste Form eines Konstruktors ist eine einzeilige Klassendefinition:

// Constructors/Wombat.kt

class Wombat

fun main() {
  val wombat = Wombat()
}

In main(), das Aufrufen von Wombat() erstellt ein Wombat-Objekt. Wenn Sie aus einer anderen objektorientierten Sprache kommen, könnten Sie erwarten, hier ein new-Schlüsselwort zu sehen, aber new wäre in Kotlin überflüssig, also wurde es weggelassen.

Sie übergeben Informationen an einen Konstruktor mit einer Parameterliste, genau wie bei einer Funktion. Hier nimmt der Alien-Konstruktor ein einziges Argument entgegen:

// Constructors/Arg.kt

class Alien(name: String) {
  val greeting = "Poor $name!"
}

fun main() {
  val alien = Alien("Mr. Meeseeks")
  println(alien.greeting)
  // alien.name // Error     // [1]
}
/* Output:
Poor Mr. Meeseeks!
*/

Um ein Alien-Objekt zu erstellen, ist ein Argument erforderlich (versuchen Sie es ohne eines). name initialisiert die greeting-Eigenschaft innerhalb des Konstruktors, ist jedoch außerhalb des Konstruktors nicht zugänglich – versuchen Sie, die Zeile [1] zu entkommentieren.

Wenn Sie möchten, dass der Konstruktor-Parameter außerhalb des Klassenkörpers zugänglich ist, definieren Sie ihn als var oder val in der Parameterliste:

// Constructors/VisibleArgs.kt

class MutableNameAlien(var name: String)

class FixedNameAlien(val name: String)

fun main() {
  val alien1 =
    MutableNameAlien("Reverse Giraffe")
  val alien2 =
    FixedNameAlien("Krombopulos Michael")

  alien1.name = "Parasite"
  // Can't do this:
  // alien2.name = "Parasite"
}

Diese Klassendefinitionen haben keine expliziten Klassenkörper—die Körper sind implizit.

Wenn name als var oder val definiert wird, wird es zu einer Eigenschaft und ist somit außerhalb des Konstruktors zugänglich. val-Konstruktorparameter können nicht geändert werden, während var-Konstruktorparameter veränderbar sind.

Ihre Klasse kann zahlreiche Konstruktorparameter haben:

// Constructors/MultipleArgs.kt

class AlienSpecies(
  val name: String,
  val eyes: Int,
  val hands: Int,
  val legs: Int
) {
  fun describe() =
    "$name with $eyes eyes, " +
      "$hands hands and $legs legs"
}

fun main() {
  val kevin =
    AlienSpecies("Zigerion", 2, 2, 2)
  val mortyJr =
    AlienSpecies("Gazorpian", 2, 6, 2)
  println(kevin.describe())
  println(mortyJr.describe())
}
/* Output:
Zigerion with 2 eyes, 2 hands and 2 legs
Gazorpian with 2 eyes, 6 hands and 2 legs
*/

In Komplexe Konstruktoren, werden Sie sehen, dass Konstruktoren auch komplexe Initialisierungslogik enthalten können.

Wenn ein Objekt verwendet wird, wenn ein String erwartet wird, ruft Kotlin die toString()-Mitgliedsfunktion des Objekts auf. Wenn Sie keine schreiben, erhalten Sie trotzdem eine Standard-toString():

// Constructors/DisplayAlienSpecies.kt

fun main() {
  val krombopulosMichael =
    AlienSpecies("Gromflomite", 2, 2, 2)
  println(krombopulosMichael)
}
/* Sample output:
AlienSpecies@4d7e1886
*/

Der Standard-toString() ist nicht sehr nützlich—er gibt den Klassennamen und die physikalische Adresse des Objekts aus (dies variiert von einer Programmausführung zur nächsten). Sie können Ihre eigene toString() definieren:

// Constructors/Scientist.kt

class Scientist(val name: String) {
  override fun toString() =
    "Scientist('$name')"
}

fun main() {
  val zeep = Scientist("Zeep Xanflorp")
  println(zeep)
}
/* Output:
Scientist('Zeep Xanflorp')
*/

override ist ein neues Schlüsselwort für uns. Es ist hier erforderlich, weil toString() bereits eine Definition hat, die eine primitive Ausgabe erzeugt. override teilt Kotlin mit, dass wir tatsächlich die Standarddefinition von toString() durch unsere eigene Definition ersetzen möchten. Die Deutlichkeit von override macht den Code klarer und verhindert Fehler.

Ein toString(), das den Inhalt eines Objekts in einer praktischen Form anzeigt, ist nützlich, um Programmierfehler zu finden und zu beheben. Um den Prozess des Debuggens zu vereinfachen, bieten IDEs Debugger an, die es Ihnen ermöglichen, jeden Schritt der Programmausführung zu beobachten und in Ihre Objekte hinein zu sehen.

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

Einschränkung der Sichtbarkeit

Wenn Sie ein Stück Code für ein paar Tage oder Wochen liegen lassen und dann zurückkehren, sehen Sie möglicherweise eine viel bessere Möglichkeit, es zu schreiben.

Dies ist eine der Hauptmotivationen für das Refactoring, das funktionierenden Code umschreibt, um ihn lesbarer, verständlicher und damit wartbarer zu machen.

Es gibt eine Spannung in diesem Wunsch, Ihren Code zu ändern und zu verbessern. Verbraucher (Anwenderprogrammierer) erfordern, dass Aspekte Ihres Codes stabil bleiben. Sie möchten es ändern, und sie wollen, dass es gleich bleibt.

Dies ist besonders wichtig für Bibliotheken. Verbraucher einer Bibliothek wollen nicht den Code für eine neue Version dieser Bibliothek umschreiben. Der Bibliotheksentwickler muss jedoch frei sein, Änderungen und Verbesserungen vorzunehmen, mit der Gewissheit, dass der Client-Code von diesen Änderungen nicht betroffen sein wird.

Daher ist eine primäre Überlegung im Softwaredesign:

Trenne Dinge, die sich ändern, von Dingen, die gleich bleiben.

Um die Sichtbarkeit zu steuern, bieten Kotlin und einige andere Sprachen Zugriffsmodifikatoren. Bibliotheksentwickler entscheiden mit den Modifikatoren public, private, protected und internal, was für den Anwenderprogrammierer zugänglich ist und was nicht. Dieses Kapitel behandelt public und private, mit einer kurzen Einführung in internal. Wir erklären protected später im Buch.

Ein Zugriffsmodifikator wie private erscheint vor der Definition einer Klasse, Funktion oder Eigenschaft. Ein Zugriffsmodifikator steuert nur den Zugriff für diese spezielle Definition.

Eine public Definition ist für Anwenderprogrammierer zugänglich, sodass Änderungen an dieser Definition den Client-Code direkt beeinflussen. Wenn Sie keinen Modifikator angeben, ist Ihre Definition automatisch public, daher ist public technisch gesehen redundant. Manchmal geben Sie dennoch public zur Klarstellung an.

Eine private Definition ist verborgen und nur von anderen Mitgliedern derselben Klasse zugänglich. Änderungen oder sogar das Entfernen einer private Definition beeinflussen die Anwenderprogrammierer nicht direkt.

private Klassen, Top-Level-Funktionen und Top-Level-Eigenschaften sind nur innerhalb dieser Datei zugänglich:

// Visibility/RecordAnimals.kt

private var index = 0                  // [1]

private class Animal(val name: String) // [2]

private fun recordAnimal(              // [3]
  animal: Animal
) {
  println("Animal #$index: ${animal.name}")
  index++
}

fun recordAnimals() {
  recordAnimal(Animal("Tiger"))
  recordAnimal(Animal("Antelope"))
}

fun recordAnimalsCount() {
  println("$index animals are here!")
}

Sie können auf private Top-Level-Eigenschaften ([1]), Klassen ([2]) und Funktionen ([3]) von anderen Funktionen und Klassen innerhalb von RecordAnimals.kt zugreifen. Kotlin verhindert, dass Sie auf ein private Top-Level-Element aus einer anderen Datei zugreifen, indem es Ihnen mitteilt, dass es in der Datei private ist:

// Visibility/ObserveAnimals.kt

fun main() {
  // Can't access private members
  // declared in another file.
  // Class is private:
  // val rabbit = Animal("Rabbit")
  // Function is private:
  // recordAnimal(rabbit)
  // Property is private:
  // index++

  recordAnimals()
  recordAnimalsCount()
}
/* Output:
Animal #0: Tiger
Animal #1: Antelope
2 animals are here!
*/

Sichtbarkeit wird am häufigsten für Mitglieder einer Klasse verwendet:

// Visibility/Cookie.kt

class Cookie(
  private var isReady: Boolean  // [1]
) {
  private fun crumble() =       // [2]
    println("crumble")

  public fun bite() =           // [3]
    println("bite")

  fun eat() {                   // [4]
    isReady = true              // [5]
    crumble()
    bite()
  }
}

fun main() {
  val x = Cookie(false)
  x.bite()
  // Can't access private members:
  // x.isReady
  // x.crumble()
  x.eat()
}
/* Output:
bite
crumble
bite
*/
  • [1] Eine private Eigenschaft, die außerhalb der umgebenden Klasse nicht zugänglich ist.
  • [2] Eine private Mitgliedsfunktion.
  • [3] Eine public Mitgliedsfunktion, die für jeden zugänglich ist.
  • [4] Kein Zugriffsmodifikator bedeutet public.
  • [5] Nur Mitglieder derselben Klasse können auf private Mitglieder zugreifen.

Das Schlüsselwort private bedeutet, dass niemand auf dieses Mitglied zugreifen kann, außer anderen Mitgliedern dieser Klasse. Andere Klassen können nicht auf private Mitglieder zugreifen, sodass es so ist, als würden Sie die Klasse auch gegen sich selbst und Ihre Mitarbeiter abschirmen. Mit private können Sie dieses Mitglied nach Belieben ändern, ohne sich Sorgen machen zu müssen, ob es eine andere Klasse im selben Paket betrifft. Als Bibliotheksentwickler werden Sie typischerweise so viel wie möglich als private halten und nur Funktionen und Klassen für die Benutzerprogrammierer freigeben.

Jede Mitgliedsfunktion, die eine Hilfsfunktion für eine Klasse ist, kann private gemacht werden, um sicherzustellen, dass Sie sie nicht versehentlich anderswo im Paket verwenden und sich dadurch daran hindern, diese Funktion zu ändern oder zu entfernen.

Dasselbe gilt für eine private Eigenschaft innerhalb einer Klasse. Es sei denn, Sie müssen die zugrunde liegende Implementierung offenlegen (was weniger wahrscheinlich ist, als Sie vielleicht denken), machen Sie Eigenschaften private. Allerdings bedeutet eine private Referenz auf ein Objekt innerhalb einer Klasse nicht, dass ein anderes Objekt nicht eine public Referenz auf dasselbe Objekt haben kann:

// Visibility/MultipleRef.kt

class Counter(var start: Int) {
  fun increment() {
    start += 1
  }
  override fun toString() = start.toString()
}

class CounterHolder(counter: Counter) {
  private val ctr = counter
  override fun toString() =
    "CounterHolder: " + ctr
}

fun main() {
  val c = Counter(11)                 // [1]
  val ch = CounterHolder(c)           // [2]
  println(ch)
  c.increment()                       // [3]
  println(ch)
  val ch2 = CounterHolder(Counter(9)) // [4]
  println(ch2)
}
/* Output:
CounterHolder: 11
CounterHolder: 12
CounterHolder: 9
*/
  • [1] c ist jetzt im Geltungsbereich definiert, der die Erstellung des CounterHolder-Objekts in der folgenden Zeile umgibt.
  • [2] c als Argument an den CounterHolder-Konstruktor zu übergeben, bedeutet, dass der neue CounterHolder nun auf dasselbe Counter-Objekt verweist, auf das auch c verweist.
  • [3] Der Counter, der angeblich privat innerhalb von ch ist, kann dennoch über c manipuliert werden.
  • [4] Counter(9) hat keine anderen Referenzen außer innerhalb von CounterHolder, daher kann es nicht von etwas anderem als ch2 zugegriffen oder modifiziert werden.

Mehrere Referenzen auf ein einzelnes Objekt zu haben, wird als Aliasing bezeichnet und kann überraschendes Verhalten hervorrufen.

Module

Im Gegensatz zu den kleinen Beispielen in diesem Buch sind reale Programme oft groß. Es kann hilfreich sein, solche Programme in ein oder mehrere Module zu unterteilen. Ein Modul ist ein logisch unabhängiger Teil eines Codebestands. Die Art und Weise, wie Sie ein Projekt in Module unterteilen, hängt vom Build-System ab (wie Gradle oder Maven) und liegt außerhalb des Rahmens dieses Buches.

Eine interne Definition ist nur innerhalb des Moduls zugänglich, in dem sie definiert ist. Intern liegt irgendwo zwischen privat und öffentlich—verwenden Sie es, wenn privat zu restriktiv ist, Sie aber nicht möchten, dass ein Element Teil der öffentlichen API ist. Wir verwenden intern nicht in den Beispielen oder Übungen des Buches.

Module sind ein höheres Konzept. Der folgende Abschnitt führt Pakete ein, die eine feinere Strukturierung ermöglichen. Eine Bibliothek ist oft ein einziges Modul, das aus mehreren Paketen besteht, sodass interne Elemente innerhalb der Bibliothek verfügbar sind, jedoch nicht von den Verbrauchern dieser Bibliothek zugänglich sind.

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

Pakete

Ein grundlegendes Prinzip in der Programmierung ist das Akronym DRY: Wiederhole dich nicht.

Mehrere identische Codefragmente erfordern Wartung, wann immer Sie Korrekturen oder Verbesserungen vornehmen. Das Duplizieren von Code ist also nicht nur zusätzliche Arbeit—jede Duplikation bietet Chancen für Fehler.

Das Schlüsselwort import wiederverwendet Code aus anderen Dateien. Eine Möglichkeit, import zu verwenden, besteht darin, einen Klassen-, Funktions- oder Eigenschaftsnamen anzugeben:

import packagename.ClassName
import packagename.functionName
import packagename.propertyName

Ein Paket ist eine zugehörige Sammlung von Code. Jedes Paket ist normalerweise dafür ausgelegt, ein bestimmtes Problem zu lösen, und enthält oft mehrere Funktionen und Klassen. Zum Beispiel können wir mathematische Konstanten und Funktionen aus der kotlin.math Bibliothek importieren:

// Packages/ImportClass.kt
import kotlin.math.PI
import kotlin.math.cos  // Cosine

fun main() {
  println(PI)
  println(cos(PI))
  println(cos(2 * PI))
}
/* Output:
3.141592653589793
-1.0
1.0
*/

Manchmal möchte man mehrere Drittanbieter-Bibliotheken verwenden, die Klassen oder Funktionen mit demselben Namen enthalten. Das Schlüsselwort as ermöglicht es Ihnen, beim Importieren Namen zu ändern:

// Packages/ImportNameChange.kt
import kotlin.math.PI as circleRatio
import kotlin.math.cos as cosine

fun main() {
  println(circleRatio)
  println(cosine(circleRatio))
  println(cosine(2 * circleRatio))
}
/* Output:
3.141592653589793
-1.0
1.0
*/

as ist nützlich, wenn der Bibliotheksname schlecht gewählt oder übermäßig lang ist.

Sie können einen Import im Hauptteil Ihres Codes vollständig qualifizieren. Im folgenden Beispiel könnte der Code aufgrund der expliziten Paketnamen weniger lesbar sein, aber die Herkunft jedes Elements ist absolut klar:

// Packages/FullyQualify.kt

fun main() {
  println(kotlin.math.PI)
  println(kotlin.math.cos(kotlin.math.PI))
  println(kotlin.math.cos(2 * kotlin.math.PI))
}
/* Output:
3.141592653589793
-1.0
1.0
*/

Um alles aus einem Paket zu importieren, verwenden Sie einen Stern:

// Packages/ImportEverything.kt
import kotlin.math.*

fun main() {
  println(E)
  println(E.roundToInt())
  println(E.toInt())
}
/* Output:
2.718281828459045
3
2
*/

Das kotlin.math-Paket enthält eine praktische Funktion roundToInt(), die den Double-Wert auf die nächste ganze Zahl aufrundet, im Gegensatz zu toInt(), das einfach alles nach einem Dezimalpunkt abschneidet.

Um Ihren Code wiederzuverwenden, erstellen Sie ein Paket mit dem Schlüsselwort package. Die package-Anweisung muss die erste nicht-kommentare Anweisung in der Datei sein. package wird gefolgt vom Namen Ihres Pakets, der konventionell komplett in Kleinbuchstaben geschrieben wird:

// Packages/PythagoreanTheorem.kt
package pythagorean
import kotlin.math.sqrt

class RightTriangle(
  val a: Double,
  val b: Double
) {
  fun hypotenuse() = sqrt(a * a + b * b)
  fun area() = a * b / 2
}

Sie können die Quellcodedatei beliebig benennen, im Gegensatz zu Java, das erfordert, dass der Dateiname mit dem Klassennamen identisch ist.

Kotlin erlaubt Ihnen, einen beliebigen Namen für Ihr Paket zu wählen, aber es wird als guter Stil betrachtet, wenn der Paketname mit dem Verzeichnisnamen identisch ist, in dem sich die Paketdateien befinden (dies wird nicht immer der Fall für die Beispiele in diesem Buch sein).

Die Elemente im pythagorean Paket sind jetzt mit import verfügbar:

// Packages/ImportPythagorean.kt
import pythagorean.RightTriangle

fun main() {
  val rt = RightTriangle(3.0, 4.0)
  println(rt.hypotenuse())
  println(rt.area())
}
/* Output:
5.0
6.0
*/

Im Rest dieses Buches verwenden wir package-Anweisungen für jede Datei, die Funktionen, Klassen usw. außerhalb von main() definiert, um Namenskonflikte mit anderen Dateien im Buch zu vermeiden. In der Regel werden wir jedoch keine package-Anweisung in einer Datei platzieren, die nur ein main() enthält.

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

Testen

Konstantes Testen ist unerlässlich für eine schnelle Programmentwicklung.

Wenn das Ändern eines Teils Ihres Codes anderen Code beschädigt, zeigen Ihre Tests das Problem sofort auf. Wenn Sie es nicht sofort herausfinden, häufen sich die Änderungen an und Sie können nicht mehr feststellen, welche Änderung das Problem verursacht hat. Sie werden viel länger brauchen, um es zu verfolgen.

Testen ist eine entscheidende Praxis, daher führen wir es früh ein und verwenden es im gesamten Rest des Buches. Auf diese Weise gewöhnen Sie sich daran, Tests als Standardteil des Programmierprozesses zu betrachten.

println() zu verwenden, um die Korrektheit des Codes zu überprüfen, ist ein schwacher Ansatz—Sie müssen jedes Mal die Ausgabe genau prüfen und bewusst sicherstellen, dass sie korrekt ist.

Um Ihre Erfahrung beim Verwenden dieses Buches zu vereinfachen, haben wir unser eigenes kleines Testsystem erstellt. Das Ziel ist ein minimaler Ansatz, der:

  1. Das erwartete Ergebnis von Ausdrücken zeigt.
  2. Eine Ausgabe liefert, damit Sie wissen, dass das Programm läuft, selbst wenn alle Tests erfolgreich sind.
  3. Das Konzept des Testens frühzeitig in Ihrer Praxis verankert.

Obwohl es für dieses Buch nützlich ist, ist unseres kein Testsystem für den Arbeitsplatz. Andere haben lange und hart gearbeitet, um solche Testsysteme zu erstellen. Zum Beispiel:

  • JUnit ist eines der beliebtesten Java-Testframeworks und kann leicht in Kotlin verwendet werden.
  • Kotest ist speziell für Kotlin entwickelt und nutzt die Funktionen der Kotlin-Sprache.
  • Das Spek Framework produziert eine andere Form des Testens, genannt Spezifikationstests.

Um unser Testframework zu verwenden, müssen wir es zuerst importieren. Die grundlegenden Elemente des Frameworks sind eq (gleich) und neq (nicht gleich):

// Testing/TestingExample.kt
import atomictest.*

fun main() {
  val v1 = 11
  val v2 = "Ontology"

  // 'eq' means "equals":
  v1 eq 11
  v2 eq "Ontology"

  // 'neq' means "not equal"
  v2 neq "Epistemology"

  // [Error] Epistemology != Ontology
  // v2 eq "Epistemology"
}
/* Output:
11
Ontology
Ontology
*/

Der Code für das Paket atomictest befindet sich in Appendix A: AtomicTest. Es ist nicht beabsichtigt, dass Sie alles in AtomicTest.kt sofort verstehen, da es einige Funktionen verwendet, die erst später im Buch erscheinen werden.

Um ein klares, angenehmes Erscheinungsbild zu erzeugen, verwendet AtomicTest eine Kotlin-Funktion, die Sie noch nicht gesehen haben: die Fähigkeit, einen Funktionsaufruf a.function(b) in der textähnlichen Form a function b zu schreiben. Dies wird als Infix-Notation bezeichnet. Nur Funktionen, die mit dem Schlüsselwort infix definiert sind, können auf diese Weise aufgerufen werden. AtomicTest.kt definiert die infix-Funktionen eq und neq, die in TestingExample.kt verwendet werden:

expression eq expected
expression neq expected

eq und neq sind flexibel – fast alles funktioniert als Testausdruck. Wenn erwartet ein String ist, wird Ausdruck in einen String umgewandelt und die beiden Strings werden verglichen. Andernfalls werden Ausdruck und erwartet direkt verglichen (ohne sie vorher umzuwandeln). In jedem Fall erscheint das Ergebnis von Ausdruck auf der Konsole, sodass Sie etwas sehen, wenn das Programm läuft. Selbst wenn die Tests erfolgreich sind, sehen Sie das Ergebnis links von eq oder neq. Wenn Ausdruck und erwartet nicht gleichwertig sind, zeigt AtomicTest einen Fehler an, wenn das Programm läuft.

Der letzte Test in TestingExample.kt schlägt absichtlich fehl, damit Sie ein Beispiel für eine Fehlerausgabe sehen. Wenn die beiden Werte nicht gleich sind, zeigt Kotlin die entsprechende Nachricht an, die mit [Error] beginnt. Wenn Sie die letzte Zeile auskommentieren und das obige Beispiel ausführen, sehen Sie nach allen erfolgreichen Tests:

[Error] Epistemology != Ontology

Der tatsächliche Wert, der in v2 gespeichert ist, entspricht nicht dem, was im Ausdruck “erwartet” behauptet wird. AtomicTest zeigt die String-Darstellungen sowohl für erwartete als auch für tatsächliche Werte an.

eq und neq sind die grundlegenden (infix) Funktionen, die für AtomicTest definiert sind – es ist wirklich ein minimalistisches Testsystem. Wenn Sie eq- und neq-Ausdrücke in Ihren Beispielen verwenden, erstellen Sie sowohl einen Test als auch eine Konsolenausgabe. Sie überprüfen die Korrektheit des Programms, indem Sie es ausführen.

Es gibt ein zweites Werkzeug in AtomicTest. Das trace-Objekt erfasst die Ausgabe für einen späteren Vergleich:

// Testing/Trace1.kt
import atomictest.*

fun main() {
  trace("line 1")
  trace(47)
  trace("line 2")
  trace eq """
    line 1
    47
    line 2
  """
}

Das Hinzufügen von Ergebnissen zu trace sieht aus wie ein Funktionsaufruf, daher können Sie println() effektiv durch trace() ersetzen.

In früheren Atomen haben wir die Ausgabe angezeigt und uns auf die menschliche visuelle Inspektion verlassen, um Unstimmigkeiten zu erkennen. Das ist unzuverlässig; selbst in einem Buch, in dem wir den Code immer wieder genau prüfen, haben wir gelernt, dass man der visuellen Inspektion nicht trauen kann, um Fehler zu finden. Von nun an verwenden wir selten kommentierte Ausgabeblöcke, da AtomicTest alles für uns erledigen wird. Manchmal fügen wir jedoch immer noch kommentierte Ausgabeblöcke ein, wenn dies einen nützlicheren Effekt hat.

Die Vorteile des Testens im gesamten restlichen Buch sollten Ihnen helfen, das Testen in Ihren Programmierprozess zu integrieren. Sie werden sich wahrscheinlich unwohl fühlen, wenn Sie Code sehen, der keine Tests hat. Sie könnten sogar entscheiden, dass Code ohne Tests per Definition fehlerhaft ist.

Testen als Teil der Programmierung

Testen ist am effektivsten, wenn es in Ihren Softwareentwicklungsprozess integriert ist. Das Schreiben von Tests stellt sicher, dass Sie die erwarteten Ergebnisse erhalten. Viele Leute befürworten das Schreiben von Tests vor dem Schreiben des Implementierungscodes - Sie lassen zuerst den Test fehlschlagen, bevor Sie den Code schreiben, um ihn erfolgreich zu machen. Diese Technik, genannt Testgetriebene Entwicklung (TDD), ist eine Möglichkeit sicherzustellen, dass Sie wirklich das testen, was Sie denken. Eine vollständigere Beschreibung von TDD finden Sie auf Wikipedia (suchen Sie nach “Testgetriebene Entwicklung”).

Es gibt einen weiteren Vorteil beim testbaren Schreiben - es verändert die Art und Weise, wie Sie Ihren Code gestalten. Sie könnten die Ergebnisse einfach auf der Konsole anzeigen. Aber im Testdenken fragen Sie sich: “Wie werde ich das testen?” Wenn Sie eine Funktion erstellen, entscheiden Sie, dass Sie etwas aus der Funktion zurückgeben sollten, wenn auch nur, um dieses Ergebnis zu testen. Funktionen, die nichts anderes tun, als Eingaben zu nehmen und Ausgaben zu erzeugen, neigen dazu, auch bessere Designs zu erzeugen.

Hier ist ein vereinfachtes Beispiel, das TDD verwendet, um die BMI-Berechnung aus Zahlentypen zu implementieren. Zuerst schreiben wir die Tests sowie eine anfängliche Implementierung, die fehlschlägt (weil wir die Funktionalität noch nicht implementiert haben):

// Testing/TDDFail.kt
package testing1
import atomictest.eq

fun main() {
  calculateBMI(160, 68) eq "Normal weight"
//  calculateBMI(100, 68) eq "Underweight"
//  calculateBMI(200, 68) eq "Overweight"
}

fun calculateBMI(lbs: Int, height: Int) =
  "Normal weight"

Nur der erste Test besteht. Die anderen Tests schlagen fehl und sind kommentiert. Als nächstes fügen wir Code hinzu, um zu bestimmen, welche Gewichte in welchen Kategorien sind. Jetzt schlagen alle Tests fehl:

// Testing/TDDStillFails.kt
package testing2
import atomictest.eq

fun main() {
  // Everything fails:
  // calculateBMI(160, 68) eq "Normal weight"
  // calculateBMI(100, 68) eq "Underweight"
  // calculateBMI(200, 68) eq "Overweight"
}

fun calculateBMI(
  lbs: Int,
  height: Int
): String {
  val bmi = lbs / (height * height) * 703.07
  return if (bmi < 18.5) "Underweight"
  else if (bmi < 25) "Normal weight"
  else "Overweight"
}

Wir verwenden Ints anstelle von Doubles, was zu einem Nullergebnis führt. Die Tests führen uns zur Lösung:

// Testing/TDDWorks.kt
package testing3
import atomictest.eq

fun main() {
  calculateBMI(160.0, 68.0) eq "Normal weight"
  calculateBMI(100.0, 68.0) eq "Underweight"
  calculateBMI(200.0, 68.0) eq "Overweight"
}

fun calculateBMI(
  lbs: Double,
  height: Double
): String {
  val bmi = lbs / (height * height) * 703.07
  return if (bmi < 18.5) "Underweight"
  else if (bmi < 25) "Normal weight"
  else "Overweight"
}

Sie können zusätzliche Tests für die Randbedingungen hinzufügen.

In den Übungen für dieses Buch haben wir Tests enthalten, die Ihr Code bestehen muss.

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

Ausnahmen

Das Wort “Ausnahme” wird im gleichen Sinne verwendet wie der Ausdruck “Ich nehme Anstoß daran.”

Eine außergewöhnliche Bedingung verhindert die Fortsetzung der aktuellen Funktion oder des aktuellen Bereichs. An dem Punkt, an dem das Problem auftritt, wissen Sie möglicherweise nicht, was Sie damit tun sollen, aber Sie können im aktuellen Kontext nicht fortfahren. Sie haben nicht genügend Informationen, um das Problem zu beheben. Daher müssen Sie stoppen und das Problem an einen anderen Kontext übergeben, der geeignete Maßnahmen ergreifen kann.

Dieses Atom behandelt die Grundlagen von Ausnahmen als ein Mechanismus zur Fehlerberichterstattung. In Abschnitt VI: Fehlervermeidung betrachten wir andere Möglichkeiten, mit Problemen umzugehen.

Es ist wichtig, eine außergewöhnliche Bedingung von einem normalen Problem zu unterscheiden. Ein normales Problem verfügt über genügend Informationen im aktuellen Kontext, um das Problem zu bewältigen. Bei einer außergewöhnlichen Bedingung können Sie die Verarbeitung nicht fortsetzen. Alles, was Sie tun können, ist zu gehen und das Problem einem externen Kontext zu überlassen. Dies ist der Fall, wenn Sie eine Ausnahme werfen. Die Ausnahme ist das Objekt, das vom Ort des Fehlers “geworfen” wird.

Betrachten Sie toInt(), das einen String in einen Int umwandelt. Was passiert, wenn Sie diese Funktion für einen String aufrufen, der keinen ganzzahligen Wert enthält?

// Exceptions/ToIntException.kt
package exceptions

fun erroneousCode() {
  // Uncomment this line to get an exception:
  // val i = "1$".toInt()        // [1]
}

fun main() {
  erroneousCode()
}

Das Auskommentieren der Zeile [1] führt zu einer Ausnahme. Hier ist die fehlerhafte Zeile kommentiert, damit der Aufbau des Buches nicht gestoppt wird, das überprüft, ob jedes Beispiel wie erwartet kompiliert und ausgeführt wird.

Wenn eine Ausnahme ausgelöst wird, stoppt der Ausführungspfad—derjenige, der nicht fortgesetzt werden kann—und das Ausnahmeobjekt wird aus dem aktuellen Kontext herausgeworfen. Hier verlässt es den Kontext von erroneousCode() und geht in den Kontext von main(). In diesem Fall meldet Kotlin nur den Fehler; der Programmierer hat vermutlich einen Fehler gemacht und muss den Code korrigieren.

Wenn eine Ausnahme nicht abgefangen wird, bricht das Programm ab und zeigt einen Stack-Trace mit detaillierten Informationen an. Das Auskommentieren der Zeile [1] in ToIntException.kt führt zu folgendem Output:

Exception in thread "main" java.lang.NumberFormatException: For input s\
tring: "1$"
  at java.lang.NumberFormatException.forInputString(NumberFormatExcepti\
on.java:65)
  at java.lang.Integer.parseInt(Integer.java:580)
  at java.lang.Integer.parseInt(Integer.java:615)
  at ToIntExceptionKt.erroneousCode(at ToIntException.kt:6)
  at ToIntExceptionKt.main(at ToIntException.kt:10)

Der Stack-Trace liefert Details wie die Datei und die Zeile, in der die Ausnahme aufgetreten ist, sodass Sie das Problem schnell entdecken können. Die letzten beiden Zeilen zeigen das Problem: In Zeile 10 von main() rufen wir erroneousCode() auf. Dann, genauer gesagt, in Zeile 6 von erroneousCode() rufen wir toInt() auf.

Um das Kommentieren und Auskommentieren von Code zur Anzeige von Ausnahmen zu vermeiden, verwenden wir die Funktion capture() aus dem Paket AtomicTest:

// Exceptions/IntroducingCapture.kt
import atomictest.*

fun main() {
  capture {
    "1$".toInt()
  } eq "NumberFormatException: " +
    """For input string: "1$""""
}

Mit capture() vergleichen wir die generierte Ausnahme mit der erwarteten Fehlermeldung. capture() ist nicht sehr hilfreich für normale Programmierung - es ist speziell für dieses Buch entworfen, damit Sie die Ausnahme sehen und wissen können, dass die Ausgabe vom Build-System des Buches überprüft wurde.

Eine weitere Strategie, wenn Sie das erwartete Ergebnis nicht erfolgreich erzielen können, besteht darin, null zurückzugeben, eine spezielle Konstante, die “kein Wert” bedeutet. Sie können null anstelle eines Wertes jeden Typs zurückgeben. Später in Nullable Typen besprechen wir, wie null den Typ des resultierenden Ausdrucks beeinflusst.

Die Kotlin-Standardbibliothek enthält String.toIntOrNull(), das die Umwandlung durchführt, wenn der String eine ganze Zahl enthält, oder null produziert, wenn die Umwandlung unmöglich ist - null ist eine einfache Möglichkeit, einen Fehler anzuzeigen:

// Exceptions/IntroducingNull.kt
import atomictest.eq

fun main() {
  "1$".toIntOrNull() eq null
}

Angenommen, wir berechnen das durchschnittliche Einkommen über einen Zeitraum von Monaten:

// Exceptions/AverageIncome.kt
package firstversion
import atomictest.*

fun averageIncome(income: Int, months: Int) =
  income / months

fun main() {
  averageIncome(3300, 3) eq 1100
  capture {
    averageIncome(5000, 0)
  } eq "ArithmeticException: / by zero"
}

Wenn months null ist, wirft die Division in averageIncome() eine ArithmeticException. Leider sagt uns dies nichts darüber, warum der Fehler aufgetreten ist, was der Nenner bedeutet und ob er überhaupt null sein darf. Dies ist eindeutig ein Fehler im Code—averageIncome() sollte mit einem months von 0 so umgehen, dass ein Division durch Null Fehler vermieden wird.

Lassen Sie uns averageIncome() modifizieren, um mehr Informationen über die Quelle des Problems zu liefern. Wenn months null ist, können wir keinen normalen Ganzzahlwert als Ergebnis zurückgeben. Eine Strategie ist es, null zurückzugeben:

// Exceptions/AverageIncomeWithNull.kt
package withnull
import atomictest.eq

fun averageIncome(income: Int, months: Int) =
  if (months == 0)
    null
  else
    income / months

fun main() {
  averageIncome(3300, 3) eq 1100
  averageIncome(5000, 0) eq null
}

Wenn eine Funktion null zurückgeben kann, verlangt Kotlin, dass Sie das Ergebnis überprüfen, bevor Sie es verwenden (dies wird in Nullable Typen behandelt). Selbst wenn Sie nur dem Benutzer eine Ausgabe anzeigen möchten, ist es besser zu sagen: “Es sind keine vollen Monatszeiträume vergangen,” anstatt “Ihr durchschnittliches Einkommen für den Zeitraum ist: null.”

Anstatt averageIncome() mit den falschen Argumenten auszuführen, können Sie eine Ausnahme auslösen – entkommen und einen anderen Teil des Programms zwingen, das Problem zu verwalten. Sie könnten die Standard-ArithmeticException zulassen, aber es ist oft nützlicher, eine spezifische Ausnahme mit einer detaillierten Fehlermeldung zu werfen. Wenn Ihre Anwendung nach ein paar Jahren im Einsatz plötzlich eine Ausnahme auslöst, weil eine neue Funktion averageIncome() aufruft, ohne die Argumente richtig zu überprüfen, werden Sie für diese Nachricht dankbar sein:

// Exceptions/AverageIncomeWithException.kt
package properexception
import atomictest.*

fun averageIncome(income: Int, months: Int) =
  if (months == 0)
    throw IllegalArgumentException(    // [1]
      "Months can't be zero")
  else
    income / months

fun main() {
  averageIncome(3300, 3) eq 1100
  capture {
    averageIncome(5000, 0)
  } eq "IllegalArgumentException: " +
    "Months can't be zero"
}
  • [1] Beim Auslösen einer Ausnahme wird das Schlüsselwort throw gefolgt von der Ausnahme, die ausgelöst werden soll, zusammen mit allen Argumenten, die sie möglicherweise benötigt. Hier verwenden wir die Standard-Ausnahmeklasse IllegalArgumentException.

Ihr Ziel ist es, die nützlichsten Nachrichten zu generieren, um die Unterstützung Ihrer Anwendung in Zukunft zu vereinfachen. Später lernen Sie, Ihre eigenen Ausnahme-Typen zu definieren und sie spezifisch auf Ihre Umstände abzustimmen.

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

Listen

Eine List ist ein Behälter, also ein Objekt, das andere Objekte enthält.

Behälter werden auch als Sammlungen bezeichnet. Wenn wir einen grundlegenden Behälter für die Beispiele in diesem Buch benötigen, verwenden wir normalerweise eine List.

Lists sind Teil des Standard-Kotlin-Pakets, daher benötigen sie keinen import.

Das folgende Beispiel erstellt eine List, die mit Ints gefüllt ist, indem die Standardbibliotheksfunktion listOf() mit Initialisierungswerten aufgerufen wird:

// Lists/Lists.kt
import atomictest.eq

fun main() {
  val ints = listOf(99, 3, 5, 7, 11, 13)
  ints eq "[99, 3, 5, 7, 11, 13]"   // [1]

  // Select each element in the List:
  var result = ""
  for (i in ints) {                 // [2]
    result += "$i "
  }
  result eq "99 3 5 7 11 13"

  // "Indexing" into the List:
  ints[4] eq 11                     // [3]
}
  • [1] Eine List verwendet eckige Klammern, um sich selbst darzustellen.
  • [2] for-Schleifen funktionieren gut mit Lists: for(i in ints) bedeutet, dass i jeden Wert in ints erhält. Sie deklarieren val i nicht und geben auch nicht seinen Typ an; Kotlin erkennt aus dem Kontext, dass i ein for-Schleifen-Identifikator ist.
  • [3] Eckige Klammern indexieren in eine List. Eine List behält ihre Elemente in der Initialisierungsreihenfolge bei, und Sie wählen sie einzeln nach Nummer aus. Wie in den meisten Programmiersprachen beginnt Kotlin das Indexieren beim Element Null, was in diesem Fall den Wert 99 ergibt. Somit ergibt ein Index von 4 den Wert 11.

Das Vergessen, dass das Indexieren bei Null beginnt, führt zum sogenannten Eins-zu-viel-Fehler. In einer Sprache wie Kotlin wählen wir oft nicht Elemente einzeln aus, sondern iterieren stattdessen durch einen gesamten Container mit in. Dies eliminiert Eins-zu-viel-Fehler.

Wenn Sie einen Index über das letzte Element in einer List hinaus verwenden, wirft Kotlin eine ArrayIndexOutOfBoundsException:

// Lists/OutOfBounds.kt
import atomictest.*

fun main() {
  val ints = listOf(1, 2, 3)
  capture {
    ints[3]
  } contains
    listOf("ArrayIndexOutOfBoundsException")
}

Eine List kann alle verschiedenen Typen halten. Hier ist eine List von Doubles und eine List von Strings:

// Lists/ListUsefulFunction.kt
import atomictest.eq

fun main() {
  val doubles =
    listOf(1.1, 2.2, 3.3, 4.4)
  doubles.sum() eq 11.0

  val strings = listOf("Twas", "Brillig",
    "And", "Slithy", "Toves")
  strings eq listOf("Twas", "Brillig",
    "And", "Slithy", "Toves")
  strings.sorted() eq listOf("And",
    "Brillig", "Slithy", "Toves", "Twas")
  strings.reversed() eq listOf("Toves",
    "Slithy", "And", "Brillig", "Twas")
  strings.first() eq "Twas"
  strings.takeLast(2) eq
    listOf("Slithy", "Toves")
}

Dies zeigt einige der Operationen von List. Beachten Sie den Namen “sorted” anstelle von “sort”. Wenn Sie sorted() aufrufen, erzeugt es eine neue List, die die gleichen Elemente wie die alte in sortierter Reihenfolge enthält—aber es lässt die ursprüngliche List unverändert. Es “sort” zu nennen, impliziert, dass die ursprüngliche List direkt verändert wird (auch bekannt als sortiert an Ort und Stelle). In Kotlin sieht man häufig diese Tendenz, “das ursprüngliche Objekt unverändert zu lassen und ein neues Objekt zu erzeugen.” reversed() erzeugt ebenfalls eine neue List.

Parametrisierte Typen

Wir betrachten es als gute Praxis, Typinferenz zu verwenden—es neigt dazu, den Code sauberer und leichter lesbar zu machen. Manchmal jedoch beschwert sich Kotlin, dass es nicht herausfinden kann, welchen Typ es verwenden soll, und in anderen Fällen macht Explizitheit den Code verständlicher. So teilen wir Kotlin mit, welchen Typ eine List enthält:

// Lists/ParameterizedTypes.kt
import atomictest.eq

fun main() {
  // Type is inferred:
  val numbers = listOf(1, 2, 3)
  val strings =
    listOf("one", "two", "three")
  // Exactly the same, but explicitly typed:
  val numbers2: List<Int> = listOf(1, 2, 3)
  val strings2: List<String> =
    listOf("one", "two", "three")
  numbers eq numbers2
  strings eq strings2
}

Kotlin verwendet die Initialisierungswerte, um abzuleiten, dass numbers eine List von Ints enthält, während strings eine List von Strings enthält.

numbers2 und strings2 sind explizit typisierte Versionen von numbers und strings, erstellt durch das Hinzufügen der Typdeklarationen List<Int> und List<String>. Sie haben Winkelklammern noch nicht gesehen - sie kennzeichnen einen Typparameter, der es Ihnen ermöglicht zu sagen: “Dieser Container enthält ‘Parameter’-Objekte.” Wir sprechen List<Int> als “List von Int” aus.

Typparameter sind nützlich für Komponenten, die keine Container sind, aber man sieht sie oft bei containerähnlichen Objekten.

Rückgabewerte können ebenfalls Typparameter haben:

// Lists/ParameterizedReturn.kt
package lists
import atomictest.eq

// Return type is inferred:
fun inferred(p: Char, q: Char) =
  listOf(p, q)

// Explicit return type:
fun explicit(p: Char, q: Char): List<Char> =
  listOf(p, q)

fun main() {
  inferred('a', 'b') eq "[a, b]"
  explicit('y', 'z') eq "[y, z]"
}

Kotlin leitet den Rückgabetyp für inferred() ab, während explicit() den Rückgabetyp der Funktion angibt. Man kann nicht einfach sagen, dass es eine List zurückgibt; Kotlin wird beanstanden, also muss man auch den Typ-Parameter angeben. Wenn Sie den Rückgabetyp einer Funktion angeben, setzt Kotlin Ihre Absicht durch.

Schreibgeschützte und veränderbare Listen

Wenn Sie nicht ausdrücklich sagen, dass Sie eine veränderbare List möchten, erhalten Sie keine. listOf() erzeugt eine schreibgeschützte List, die keine veränderbaren Funktionen hat.

Wenn Sie eine List schrittweise erstellen (das heißt, Sie haben nicht alle Elemente zum Zeitpunkt der Erstellung), verwenden Sie mutableListOf(). Dies erzeugt eine MutableList, die verändert werden kann:

// Lists/MutableList.kt
import atomictest.eq

fun main() {
  val list = mutableListOf<Int>()

  list.add(1)
  list.addAll(listOf(2, 3))

  list += 4
  list += listOf(5, 6)

  list eq listOf(1, 2, 3, 4, 5, 6)
}

Da list keine anfänglichen Elemente hat, müssen wir Kotlin mitteilen, welchen Typ es hat, indem wir die <Int>-Spezifikation im Aufruf von mutableListOf() angeben. Sie können Elemente zu einer MutableList mit add() und addAll() hinzufügen oder den Operator += verwenden, der entweder ein einzelnes Element oder eine andere Sammlung hinzufügt.

Eine MutableList kann als List behandelt werden, in diesem Fall kann sie nicht geändert werden. Sie können jedoch eine schreibgeschützte List nicht als MutableList behandeln:

// Lists/MutListIsList.kt
package lists
import atomictest.eq

fun makeList(): List<Int> =
  mutableListOf(1, 2, 3)

fun main() {
  // makeList() produces a read-only List:
  val list = makeList()
  // list.add(3) // Unresolved reference: add
  list eq listOf(1, 2, 3)
}

list fehlt es an Mutationsfunktionen, obwohl es ursprünglich mit mutableListOf() innerhalb von makeList() erstellt wurde. Beachten Sie, dass der Ergebnistyp von makeList() List<Int> ist. Das ursprüngliche Objekt ist immer noch eine MutableList, wird aber durch die Linse einer List betrachtet.

Eine List ist schreibgeschützt—Sie können ihren Inhalt lesen, aber nicht schreiben. Wenn die zugrunde liegende Implementierung eine MutableList ist und Sie eine veränderbare Referenz auf diese Implementierung beibehalten, können Sie sie weiterhin über diese veränderbare Referenz modifizieren, und alle schreibgeschützten Referenzen werden diese Änderungen sehen. Dies ist ein weiteres Beispiel für Aliasing, eingeführt in Einschränken der Sichtbarkeit:

// Lists/MultipleListRefs.kt
import atomictest.eq

fun main() {
  val first = mutableListOf(1)
  val second: List<Int> = first
  second eq listOf(1)
  first.add(2)
  // second sees the change:
  second eq listOf(1, 2)
}

first ist eine unveränderliche Referenz (val) auf das veränderliche Objekt, das von mutableListOf(1) erzeugt wird. Wenn second auf first aliasiert wird, wird es zu einer Ansicht desselben Objekts. second ist schreibgeschützt, weil List<Int> keine Änderungsfunktionen beinhaltet. Ohne die explizite List<Int> Typdeklaration würde Kotlin annehmen, dass second ebenfalls eine Referenz auf ein veränderliches Objekt ist.

Wir können dem Objekt ein Element (2) hinzufügen, weil first eine Referenz auf eine veränderliche Liste ist. Beachten Sie, dass second diese Änderungen beobachtet—es kann die Liste nicht ändern, obwohl die Liste über first geändert wird.

Das += Rätsel

Der += Operator kann den Anschein erwecken, dass eine unveränderliche Liste tatsächlich veränderlich ist:

// Lists/ApparentlyMutableList.kt
import atomictest.eq

fun main() {
  var list = listOf('X') // Immutable
  list += 'Y' // Appears to be mutable
  list eq "[X, Y]"
}

listOf() erzeugt eine unveränderliche List, aber list += 'Y' scheint diese List zu ändern. Verstößt += irgendwie gegen die Unveränderlichkeit?

Dies passiert nur, weil list ein var ist. Hier ist ein detaillierteres Beispiel, das die verschiedenen Kombinationen von veränderlichen/unveränderlichen Lists mit val/var zeigt:

// Lists/PlusAssignPuzzle.kt
import atomictest.eq

fun main() {
    // Mutable List assigned to a 'val'/'var':
    val list1 = mutableListOf('A') // or 'var'
    list1 += 'A' // Is the same as:
    list1.plusAssign('A')               // [1]

    // Immutable List assigned to a 'val':
    val list2 = listOf('B')
    // list2 += 'B' // Is the same as:
    // list2 = list2 + 'B'              // [2]

    // Immutable List assigned to a 'var':
    var list3 = listOf('C')
    list3 += 'C' // Is the same as:
    val newList = list3 + 'C'           // [3]
    list3 = newList                     // [4]

    list1 eq "[A, A, A]"
    list2 eq "[B]"
    list3 eq "[C, C, C]"
}
  • [1] list1 bezieht sich auf ein veränderbares Objekt, das daher vor Ort modifiziert werden kann. Der Compiler übersetzt += zum Aufruf von plusAssign(). Es spielt keine Rolle, ob list1 ein val oder ein var ist, da list1 nach der Erstellung niemals neu zugewiesen wird—es verweist immer auf die gleiche veränderbare Liste. Wenn man es zu einem var macht, weist IntelliJ darauf hin, dass es sich nie ändert und schlägt vor, es zu einem val zu machen.
  • [2] Dies versucht, eine neue List zu erstellen, indem list2 und 'B' kombiniert werden, aber es kann diese neue List nicht list2 neu zuweisen, da list2 ein val ist. Ohne die Möglichkeit, diese Neuzuweisung durchzuführen, kann += nicht kompiliert werden.
  • [3] Erstellt newList ohne die bestehende unveränderliche List zu modifizieren, auf die list3 verweist.
  • [4] Da list3 ein var ist, weist der Compiler newList zurück in list3 zu. Der vorherige Inhalt von list3 wird dann vergessen, und es erscheint, als ob list3 verändert wurde. Tatsächlich wurde das alte list3 verworfen und durch das neu erstellte newList ersetzt, was die Illusion erzeugt, dass list3 veränderbar ist.

Dieses Verhalten von += tritt auch bei anderen Sammlungen auf. Die daraus resultierende Verwirrung ist ein weiterer Grund, val gegenüber var für Ihre Bezeichner zu bevorzugen.

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

Variable Argumentlisten

Das Schlüsselwort vararg erzeugt eine flexibel dimensionierte Argumentliste.

In Listen haben wir listOf() eingeführt, das eine beliebige Anzahl von Parametern akzeptiert und eine Liste erzeugt:

// Varargs/ListOf.kt
import atomictest.eq

fun main() {
  listOf(1) eq "[1]"
  listOf("a", "b") eq "[a, b]"
}

Mit dem Schlüsselwort vararg können Sie eine Funktion definieren, die eine beliebige Anzahl von Argumenten annimmt, genau wie listOf(). vararg ist die Abkürzung für variable Argumentliste:

// Varargs/VariableArgList.kt
package varargs

fun v(s: String, vararg d: Double) {}

fun main() {
  v("abc", 1.0, 2.0)
  v("def", 1.0, 2.0, 3.0, 4.0)
  v("ghi", 1.0, 2.0, 3.0, 4.0, 5.0, 6.0)
}

Eine Funktionsdefinition kann nur einen Parameter als vararg angeben. Obwohl es möglich ist, ein beliebiges Element in der Parameterliste als vararg anzugeben, ist es normalerweise am einfachsten, dies für das letzte zu tun.

vararg ermöglicht es Ihnen, eine beliebige Anzahl (einschließlich null) von Argumenten zu übergeben. Alle Argumente müssen vom angegebenen Typ sein. Auf vararg-Argumente wird mit dem Parameternamen zugegriffen, der zu einem Array wird:

// Varargs/VarargSum.kt
package varargs
import atomictest.eq

fun sum(vararg numbers: Int): Int {
  var total = 0
  for (n in numbers) {
    total += n
  }
  return total
}

fun main() {
  sum(13, 27, 44) eq 84
  sum(1, 3, 5, 7, 9, 11) eq 36
  sum() eq 0
}

Obwohl Arrays und Lists ähnlich aussehen, sind sie unterschiedlich implementiert — List ist eine reguläre Bibliotheksklasse, während Array spezielle Unterstützung auf niedriger Ebene hat. Array stammt aus der Anforderung von Kotlin, mit anderen Sprachen, insbesondere Java, kompatibel zu sein.

Im täglichen Programmieren verwenden Sie eine List, wenn Sie eine einfache Sequenz benötigen. Verwenden Sie Arrays nur, wenn eine Drittanbieter-API ein Array erfordert oder wenn Sie mit varargs arbeiten.

In den meisten Fällen können Sie einfach ignorieren, dass vararg ein Array erzeugt, und es behandeln, als ob es eine List wäre:

// Varargs/VarargLikeList.kt
package varargs
import atomictest.eq

fun evaluate(vararg ints: Int) =
  "Size: ${ints.size}\n" +
  "Sum: ${ints.sum()}\n" +
  "Average: ${ints.average()}"

fun main() {
  evaluate(10, -3, 8, 1, 9) eq """
    Size: 5
    Sum: 25
    Average: 5.0
  """
}

Sie können ein Array von Elementen überall dort übergeben, wo ein vararg akzeptiert wird. Um ein Array zu erstellen, verwenden Sie arrayOf() auf die gleiche Weise wie listOf(). Ein Array ist immer veränderbar. Um ein Array in eine Folge von Argumenten (nicht nur ein einzelnes Element des Typs Array) zu konvertieren, verwenden Sie den Spread-Operator, *:

// Varargs/SpreadOperator.kt
import varargs.sum
import atomictest.eq

fun main() {
  val array = intArrayOf(4, 5)
  sum(1, 2, 3, *array, 6) eq 21  // [1]
  // Doesn't compile:
  // sum(1, 2, 3, array, 6)

  val list = listOf(9, 10, 11)
  sum(*list.toIntArray()) eq 30  // [2]
}

Wenn Sie ein Array von primitiven Typen (wie Int, Double oder Boolean) wie im obigen Beispiel übergeben, muss die Array-Erstellungsfunktion spezifisch typisiert sein. Wenn Sie arrayOf(4, 5) anstelle von intArrayOf(4, 5) verwenden, wird Zeile [1] einen Fehler erzeugen, der besagt, dass der abgeleitete Typ ist Array<Int>, aber IntArray wurde erwartet.

Der Streuoperator funktioniert nur mit Arrays. Wenn Sie eine List haben, die Sie als Folge von Argumenten übergeben möchten, konvertieren Sie sie zuerst in ein Array und wenden Sie dann den Streuoperator an, wie in [2]. Da das Ergebnis ein Array eines primitiven Typs ist, müssen wir erneut die spezifische Konvertierungsfunktion toIntArray() verwenden.

Der Streuoperator ist besonders hilfreich, wenn Sie vararg-Argumente an eine andere Funktion übergeben müssen, die ebenfalls varargs erwartet:

// Varargs/TwoFunctionsWithVarargs.kt
package varargs
import atomictest.eq

fun first(vararg numbers: Int): String {
  var result = ""
  for (i in numbers) {
    result += "[$i]"
  }
  return result
}

fun second(vararg numbers: Int) =
  first(*numbers)

fun main() {
  second(7, 9, 32) eq "[7][9][32]"
}

Kommandozeilenargumente

Beim Aufrufen eines Programms auf der Kommandozeile können Sie ihm eine variable Anzahl von Argumenten übergeben. Um Kommandozeilenargumente zu erfassen, müssen Sie main() einen bestimmten Parameter bereitstellen:

// Varargs/MainArgs.kt

fun main(args: Array<String>) {
  for (a in args) {
    println(a)
  }
}

Der Parameter wird traditionell args genannt (obwohl Sie ihn beliebig nennen können), und der Typ für args kann nur Array<String> (Array von String) sein.

Wenn Sie IntelliJ IDEA verwenden, können Sie Programmargumente über die Bearbeitung der entsprechenden “Run-Konfiguration” übergeben, wie im letzten Übungsteil für dieses Atom gezeigt.

Sie können auch den kotlinc-Compiler verwenden, um ein Befehlszeilenprogramm zu erstellen. Wenn kotlinc nicht auf Ihrem Computer vorhanden ist, folgen Sie den Anweisungen auf der Kotlin-Hauptseite. Nachdem Sie den Code für MainArgs.kt eingegeben und gespeichert haben, geben Sie Folgendes an einer Eingabeaufforderung ein:

kotlinc MainArgs.kt

Sie geben die command-line arguments nach dem program invocation ein, so:

kotlin MainArgsKt hamster 42 3.14159

Sie werden diese Ausgabe sehen:

hamster
42
3.14159

Wenn Sie einen String-Parameter in einen spezifischen Typ umwandeln möchten, stellt Kotlin Konvertierungsfunktionen bereit, wie zum Beispiel toInt() für die Umwandlung in einen Int und toFloat() für die Umwandlung in einen Float. Bei der Verwendung dieser Funktionen wird angenommen, dass die Kommandozeilenargumente in einer bestimmten Reihenfolge erscheinen. Hier erwartet das Programm einen String, gefolgt von etwas, das in einen Int umwandelbar ist, gefolgt von etwas, das in einen Float umwandelbar ist:

// Varargs/MainArgConversion.kt

fun main(args: Array<String>) {
  if (args.size < 3) return
  val first = args[0]
  val second = args[1].toInt()
  val third = args[2].toFloat()
  println("$first  $second  $third")
}

Die erste Zeile in main() beendet das Programm, wenn nicht genügend Argumente vorhanden sind. Wenn Sie nichts angeben, das in ein Int und ein Float umgewandelt werden kann, als zweites und drittes Kommandozeilenargument, werden Sie Laufzeitfehler sehen (versuchen Sie es, um die Fehler zu sehen).

Kompilieren und führen Sie MainArgConversion.kt mit denselben Kommandozeilenargumenten aus, die wir zuvor verwendet haben, und Sie werden sehen:

hamster 42 3.14159

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

Mengen

Ein Set ist eine Sammlung, die nur ein Element jedes Wertes zulässt.

Die häufigste Set-Aktivität ist der Test auf Mitgliedschaft mit in oder contains():

// Sets/Sets.kt
import atomictest.eq

fun main() {
  val intSet = setOf(1, 1, 2, 3, 9, 9, 4)
  // No duplicates:
  intSet eq setOf(1, 2, 3, 4, 9)

  // Element order is unimportant:
  setOf(1, 2) eq setOf(2, 1)

  // Set membership:
  (9 in intSet) eq true
  (99 in intSet) eq false

  intSet.contains(9) eq true
  intSet.contains(99) eq false

  // Does this set contain another set?
  intSet.containsAll(setOf(1, 9, 2)) eq true

  // Set union:
  intSet.union(setOf(3, 4, 5, 6)) eq
    setOf(1, 2, 3, 4, 5, 6, 9)

  // Set intersection:
  intSet intersect setOf(0, 1, 2, 7, 8) eq
    setOf(1, 2)

  // Set difference:
  intSet subtract setOf(0, 1, 9, 10) eq
    setOf(2, 3, 4)
  intSet - setOf(0, 1, 9, 10) eq
    setOf(2, 3, 4)
}

Dieses Beispiel zeigt:

  1. Das Platzieren von doppelten Elementen in einem Set entfernt diese Duplikate automatisch.
  2. Die Reihenfolge der Elemente ist bei Mengen nicht wichtig. Zwei Mengen sind gleich, wenn sie die gleichen Elemente enthalten.
  3. Sowohl in als auch contains() testen auf Mitgliedschaft.
  4. Sie können die üblichen Venn-Diagramm-Operationen wie Überprüfung auf Teilmengen, Vereinigung, Schnittmenge und Differenz durchführen, entweder mit Punktnotation (set.union(other)) oder Infix-Notation (set intersect other). Die Funktionen union, intersect und subtract können mit Infix-Notation verwendet werden.
  5. Die Mengendifferenz kann entweder mit subtract() oder dem Minus-Operator ausgedrückt werden.

Um Duplikate aus einer Liste zu entfernen, konvertieren Sie sie in ein Set:

// Sets/RemoveDuplicates.kt
import atomictest.eq

fun main() {
  val list = listOf(3, 3, 2, 1, 2)
  list.toSet() eq setOf(1, 2, 3)
  list.distinct() eq listOf(3, 2, 1)
  "abbcc".toSet() eq setOf('a', 'b', 'c')
}

Sie können auch distinct() verwenden, das eine List zurückgibt. Sie können toSet() auf einem String aufrufen, um ihn in eine Menge einzigartiger Zeichen umzuwandeln.

Wie bei List bietet Kotlin zwei Erstellungsfunktionen für Set. Das Ergebnis von setOf() ist schreibgeschützt. Um ein veränderbares Set zu erstellen, verwenden Sie mutableSetOf():

// Sets/MutableSet.kt
import atomictest.eq

fun main() {
  val mutableSet = mutableSetOf<Int>()
  mutableSet += 42
  mutableSet += 42
  mutableSet eq setOf(42)
  mutableSet -= 42
  mutableSet eq setOf<Int>()
}

Die Operatoren += und -= fügen Elemente zu Sets hinzu bzw. entfernen sie, genau wie bei Lists.

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

Karten

Eine Map verbindet keys mit values und sucht einen Wert anhand eines Schlüssels.

Sie erstellen eine Map, indem Sie mapOf() Schlüssel-Wert-Paare bereitstellen. Mit to trennen wir jeden Schlüssel von seinem zugehörigen Wert:

// Maps/Maps.kt
import atomictest.eq

fun main() {
  val constants = mapOf(
    "Pi" to 3.141,
    "e" to 2.718,
    "phi" to 1.618
  )
  constants eq
    "{Pi=3.141, e=2.718, phi=1.618}"

  // Look up a value from a key:
  constants["e"] eq 2.718              // [1]
  constants.keys eq setOf("Pi", "e", "phi")
  constants.values eq "[3.141, 2.718, 1.618]"

  var s = ""
  // Iterate through key-value pairs:
  for (entry in constants) {           // [2]
    s += "${entry.key}=${entry.value}, "
  }
  s eq "Pi=3.141, e=2.718, phi=1.618,"

  s = ""
  // Unpack during iteration:
  for ((key, value) in constants)      // [3]
    s += "$key=$value, "
  s eq "Pi=3.141, e=2.718, phi=1.618,"
}
  • [1] Der [] Operator sucht einen Wert mithilfe eines Schlüssels. Sie können alle Schlüssel mit keys und alle Werte mit values erzeugen. Der Aufruf von keys erzeugt eine Set, da alle Schlüssel in einem Map einzigartig sein müssen, andernfalls gäbe es eine Mehrdeutigkeit bei einer Suche.
  • [2] Das Iterieren durch ein Map erzeugt Schlüssel-Wert-Paare als Mapeinträge.
  • [3] Sie können Schlüssel und Werte beim Iterieren entpacken.

Ein einfaches Map ist schreibgeschützt. Hier ist ein MutableMap:

// Maps/MutableMaps.kt
import atomictest.eq

fun main() {
  val m =
    mutableMapOf(5 to "five", 6 to "six")
  m[5] eq "five"
  m[5] = "5ive"
  m[5] eq "5ive"
  m += 4 to "four"
  m eq mapOf(5 to "5ive",
    4 to "four", 6 to "six")
}

map[key] = value fügt den Wert hinzu oder ändert ihn, der mit dem Schlüssel verknüpft ist. Sie können auch explizit ein Paar hinzufügen, indem Sie map += key to value verwenden.

mapOf() und mutableMapOf() bewahren die Reihenfolge, in der die Elemente in die Map eingefügt werden. Dies ist nicht für andere Typen von Map garantiert.

Eine schreibgeschützte Map erlaubt keine Änderungen:

// Maps/ReadOnlyMaps.kt
import atomictest.eq

fun main() {
  val m = mapOf(5 to "five", 6 to "six")
  m[5] eq "five"
  // m[5] = "5ive" // Fails
  // m += (4 to "four") // Fails
  m + (4 to "four") // Doesn't change m
  m eq mapOf(5 to "five", 6 to "six")
  val m2 = m + (4 to "four")
  m2 eq mapOf(
    5 to "five", 6 to "six", 4 to "four")
}

Die Definition von m erstellt eine Map, die Ints mit Strings verknüpft. Wenn wir versuchen, einen String zu ersetzen, gibt Kotlin einen Fehler aus.

Ein Ausdruck mit + erstellt eine neue Map, die sowohl die alten Elemente als auch das neue enthält, aber die ursprüngliche Map nicht beeinflusst. Die einzige Möglichkeit, ein Element zu einer unveränderlichen Map “hinzuzufügen”, besteht darin, eine neue Map zu erstellen.

Eine Map gibt null zurück, wenn sie keinen Eintrag für einen gegebenen Schlüssel enthält. Wenn Sie ein Ergebnis benötigen, das nicht null sein kann, verwenden Sie getValue() und fangen Sie NoSuchElementException ab, falls der Schlüssel fehlt:

// Maps/GetValue.kt
import atomictest.*

fun main() {
  val map = mapOf('a' to "attempt")
  map['b'] eq null
  capture {
    map.getValue('b')
  } eq "NoSuchElementException: " +
    "Key b is missing in the map."
  map.getOrDefault('a', "??") eq "attempt"
  map.getOrDefault('b', "??") eq "??"
}

getOrDefault() ist normalerweise eine angenehmere Alternative zu null oder einer Ausnahme.

Sie können Klasseninstanzen als Werte in einem Map speichern. Hier ist ein Map, das einen Contact anhand eines Zahlen-String abruft:

// Maps/ContactMap.kt
package maps
import atomictest.eq

class Contact(
  val name: String,
  val phone: String
) {
  override fun toString() =
    "Contact('$name', '$phone')"
}

fun main() {
  val miffy = Contact("Miffy", "1-234-567890")
  val cleo = Contact("Cleo", "098-765-4321")
  val contacts = mapOf(
    miffy.phone to miffy,
    cleo.phone to cleo)
  contacts["1-234-567890"] eq miffy
  contacts["1-111-111111"] eq null
}

Es ist möglich, Klasseninstanzen als Schlüssel in einer Map zu verwenden, aber das ist komplizierter, daher besprechen wir es später im Buch.

  • -

Maps sehen aus wie einfache kleine Datenbanken. Sie werden manchmal assoziative Arrays genannt, weil sie Schlüssel mit Werten verknüpfen. Obwohl sie im Vergleich zu einer voll ausgestatteten Datenbank ziemlich begrenzt sind, sind sie dennoch bemerkenswert nützlich (und weitaus effizienter als eine Datenbank).

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

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.

Zusammenfassung 2

Dieses Atom fasst die Atome in Abschnitt II zusammen und überprüft sie, von Objects Everywhere bis Property Accessors.

Wenn Sie ein erfahrener Programmierer sind, ist dies Ihr nächstes Atom nach Summary 1, und Sie werden die Atome danach der Reihe nach durchgehen.

Neue Programmierer sollten dieses Atom lesen und die Übungen zur Überprüfung durchführen. Wenn Ihnen hier Informationen unklar sind, gehen Sie zurück und studieren Sie das Atom zu diesem Thema.

Die Themen erscheinen in einer geeigneten Reihenfolge für erfahrene Programmierer, was nicht der gleichen Reihenfolge der Atome im Buch entspricht. Zum Beispiel beginnen wir mit der Einführung von Paketen und Importen, damit wir unser minimales Test-Framework für den Rest des Atoms verwenden können.

Pakete & Testen

Eine beliebige Anzahl von wiederverwendbaren Bibliothekskomponenten kann unter einem einzigen Bibliotheksnamen mit dem package-Schlüsselwort gebündelt werden:

// Summary2/ALibrary.kt
package com.yoururl.libraryname

// Components to reuse ...
fun f() = "result"

Sie können mehrere Komponenten in einer einzigen Datei platzieren oder Komponenten auf mehrere Dateien mit demselben Paketnamen verteilen. Hier haben wir f() als einzige Komponente definiert.

Um es eindeutig zu machen, beginnt der Paketname konventionell mit Ihrem umgekehrten Domain-Namen. In diesem Beispiel ist der Domain-Name yoururl.com.

In Kotlin kann der Paketname unabhängig von dem Verzeichnis sein, in dem sich seine Inhalte befinden. Java verlangt, dass die Verzeichnisstruktur mit dem vollqualifizierten Paketnamen übereinstimmt, sodass das Paket com.yoururl.libraryname im Verzeichnis com/yoururl/libraryname liegen sollte. Für gemischte Kotlin- und Java-Projekte empfiehlt der Kotlin-Stilführer dieselbe Praxis. Für reine Kotlin-Projekte platzieren Sie das Verzeichnis libraryname auf der obersten Ebene der Verzeichnisstruktur Ihres Projekts.

Eine Importanweisung bringt einen oder mehrere Namen in den aktuellen Namensraum:

// Summary2/UseALibrary.kt
import com.yoururl.libraryname.*

fun main() {
  val x = f()
}

Der Stern nach libraryname weist Kotlin an, alle Komponenten einer Bibliothek zu importieren. Sie können auch Komponenten einzeln auswählen; Details finden Sie unter Pakete.

Im restlichen Teil dieses Buches verwenden wir package-Anweisungen für jede Datei, die Funktionen, Klassen usw. außerhalb von main() definiert. Dies verhindert Namenskonflikte mit anderen Dateien im Buch. In der Regel setzen wir keine package-Anweisung in eine Datei, die nur ein main() enthält.

Eine wichtige Bibliothek für dieses Buch ist atomictest, unser einfaches Test-Framework. atomictest ist definiert in Anhang A: AtomicTest, obwohl es Sprachmerkmale verwendet, die Sie zu diesem Zeitpunkt im Buch noch nicht verstehen werden.

Nach dem Importieren von atomictest verwenden Sie eq (gleich) und neq (ungleich) fast so, als wären sie Sprachschlüsselwörter:

// Summary2/UsingAtomicTest.kt
import atomictest.*

fun main() {
  val pi = 3.14
  val pie = "A round dessert"
  pi eq 3.14
  pie eq "A round dessert"
  pi neq pie
}
/* Output:
3.14
A round dessert
3.14
*/

Die Fähigkeit, eq/neq ohne Punkte oder Klammern zu verwenden, wird als Infix-Notation bezeichnet. Sie können infix-Funktionen entweder auf die reguläre Weise aufrufen: pi.eq(3.14), oder unter Verwendung der Infix-Notation: pi eq 3.14. Sowohl eq als auch neq sind Wahrheitsaussagen, die das Ergebnis von der linken Seite der eq/neq-Anweisung anzeigen, sowie eine Fehlermeldung, wenn der Ausdruck auf der rechten Seite von eq nicht gleichwertig zur linken ist (oder gleichwertig ist, im Fall von neq). Auf diese Weise sehen Sie verifizierte Ergebnisse im Quellcode.

atomictest.trace verwendet die Funktionsaufruf-Syntax, um Ergebnisse hinzuzufügen, die dann mit eq validiert werden können:

// Testing/UsingTrace.kt
import atomictest.*

fun main() {
  trace("Hello,")
  trace(47)
  trace("World!")
  trace eq """
    Hello,
    47
    World!
  """
}

Sie können println() effektiv durch trace() ersetzen.

Objekte überall

Kotlin ist eine hybrid objekt-funktionale Sprache: Sie unterstützt sowohl objektorientierte als auch funktionale Programmierparadigmen.

Objekte enthalten vals und vars, um Daten zu speichern (diese werden Eigenschaften genannt) und führen Operationen mit Funktionen aus, die innerhalb einer Klasse definiert sind, sogenannte Mitgliedsfunktionen (wenn es eindeutig ist, sagen wir einfach “Funktionen”). Eine Klasse definiert Eigenschaften und Mitgliedsfunktionen für das, was im Wesentlichen ein neuer, benutzerdefinierter Datentyp ist. Wenn Sie ein val oder var einer Klasse erstellen, nennt man dies ein Objekt erstellen oder eine Instanz erstellen.

Eine besonders nützliche Art von Objekt ist der Container, auch Sammlung genannt. Ein Container ist ein Objekt, das andere Objekte hält. In diesem Buch verwenden wir oft die List, da sie die vielseitigste Sequenz ist. Hier führen wir mehrere Operationen an einer List durch, die Doubles enthält. listOf() erstellt eine neue List aus ihren Argumenten:

// Summary2/ListCollection.kt
import atomictest.eq

fun main() {
  val lst = listOf(19.2, 88.3, 22.1)
  lst[1] eq 88.3  // Indexing
  lst.reversed() eq listOf(22.1, 88.3, 19.2)
  lst.sorted() eq listOf(19.2, 22.1, 88.3)
  lst.sum() eq 129.6
}

Kein import-Statement ist erforderlich, um eine List zu verwenden.

Kotlin verwendet eckige Klammern für die Indexierung in Sequenzen. Die Indexierung beginnt bei Null.

Dieses Beispiel zeigt auch einige der vielen Standardbibliotheksfunktionen, die für Lists verfügbar sind: sorted(), reversed(), und sum(). Um diese Funktionen zu verstehen, konsultieren Sie die Kotlin Dokumentation online.

Wenn Sie sorted() oder reversed() aufrufen, wird lst nicht verändert. Stattdessen wird eine neue List erstellt und zurückgegeben, die das gewünschte Ergebnis enthält. Dieser Ansatz, das Originalobjekt niemals zu verändern, ist durchgehend in den Kotlin-Bibliotheken konsistent, und Sie sollten bestrebt sein, diesem Muster zu folgen, wenn Sie Ihren eigenen Code schreiben.

Klassen erstellen

Eine Klassendefinition besteht aus dem Schlüsselwort class, einem Namen für die Klasse und einem optionalen Körper. Der Körper enthält Eigenschaftsdefinitionen (vals und vars) und Funktionsdefinitionen.

Dieses Beispiel definiert eine NoBody-Klasse ohne Körper und Klassen mit val-Eigenschaften:

// Summary2/ClassBodies.kt
package summary2

class NoBody

class SomeBody {
  val name = "Janet Doe"
}

class EveryBody {
  val all = listOf(SomeBody(),
    SomeBody(), SomeBody())
}

fun main() {
  val nb = NoBody()
  val sb = SomeBody()
  val eb = EveryBody()
}

Um eine Instanz einer Klasse zu erstellen, setzen Sie Klammern nach ihrem Namen, sowie Argumente, falls diese erforderlich sind.

Eigenschaften innerhalb von Klassenkörpern können jeden Typ haben. SomeBody enthält eine Eigenschaft vom Typ String, und die Eigenschaft von EveryBody ist eine List, die SomeBody-Objekte hält.

Hier ist eine Klasse mit Mitgliedsfunktionen:

// Summary2/Temperature.kt
package summary2
import atomictest.eq

class Temperature {
  var current = 0.0
  var scale = "f"
  fun setFahrenheit(now: Double) {
    current = now
    scale = "f"
  }
  fun setCelsius(now: Double) {
    current = now
    scale = "c"
  }
  fun getFahrenheit(): Double =
    if (scale == "f")
      current
    else
      current * 9.0 / 5.0 + 32.0
  fun getCelsius(): Double =
    if (scale == "c")
      current
    else
      (current - 32.0) * 5.0 / 9.0
}

fun main() {
  val temp = Temperature()   // [1]
  temp.setFahrenheit(98.6)
  temp.getFahrenheit() eq 98.6
  temp.getCelsius() eq 37.0
  temp.setCelsius(100.0)
  temp.getFahrenheit() eq 212.0
}

Diese Mitgliedsfunktionen sind genau wie die auf oberster Ebene definierten Funktionen außerhalb von Klassen, außer dass sie zur Klasse gehören und uneingeschränkten Zugriff auf die anderen Mitglieder der Klasse haben, wie current und scale. Mitgliedsfunktionen können auch andere Mitgliedsfunktionen in derselben Klasse ohne Qualifikation aufrufen.

  • [1] Obwohl temp ein val ist, modifizieren wir später das Temperature-Objekt. Die val-Definition verhindert, dass die Referenz temp auf ein neues Objekt umgeschrieben wird, schränkt jedoch das Verhalten des Objekts selbst nicht ein.

Die folgenden zwei Klassen sind die Basis eines Tic-Tac-Toe-Spiels:

// Summary2/TicTacToe.kt
package summary2
import atomictest.eq

class Cell {
  var entry = ' '                   // [1]
  fun setValue(e: Char): String =   // [2]
    if (entry == ' ' &&
      (e == 'X' || e == 'O')) {
      entry = e
      "Successful move"
    } else
      "Invalid move"
}

class Grid {
  val cells = listOf(
    listOf(Cell(), Cell(), Cell()),
    listOf(Cell(), Cell(), Cell()),
    listOf(Cell(), Cell(), Cell())
  )
  fun play(e: Char, x: Int, y: Int): String =
    if (x !in 0..2 || y !in 0..2)
      "Invalid move"
    else
      cells[x][y].setValue(e)       // [3]
}

fun main() {
  val grid = Grid()
  grid.play('X', 1, 1) eq "Successful move"
  grid.play('X', 1, 1) eq "Invalid move"
  grid.play('O', 1, 3) eq "Invalid move"
}

Die Grid-Klasse enthält eine List, die drei Lists enthält, von denen jede drei Cells enthält — eine Matrix.

  • [1] Die entry-Eigenschaft in Cell ist ein var, sodass sie modifiziert werden kann. Die einfachen Anführungszeichen in der Initialisierung erzeugen einen Char-Typ, daher müssen alle Zuweisungen zu entry ebenfalls Chars sein.
  • [2] setValue() prüft, ob die Cell verfügbar ist und ob Sie das richtige Zeichen übergeben haben. Es gibt ein String-Ergebnis zurück, um Erfolg oder Misserfolg anzuzeigen.
  • [3] play() überprüft, ob die x- und y-Argumente im Bereich liegen, und indiziert dann in die Matrix, wobei es sich auf die von setValue() durchgeführten Tests stützt.

Konstruktoren

Konstruktoren erstellen neue Objekte. Sie übergeben Informationen an einen Konstruktor mithilfe seiner Parameterliste, die direkt nach dem Klassennamen in Klammern gesetzt wird. Ein Konstruktoraufruf sieht daher wie ein Funktionsaufruf aus, außer dass der Anfangsbuchstabe des Namens großgeschrieben wird (gemäß dem Kotlin-Stilguide). Der Konstruktor gibt ein Objekt der Klasse zurück:

// Summary2/WildAnimals.kt
package summary2
import atomictest.eq

class Badger(id: String, years: Int) {
  val name = id
  val age = years
  override fun toString() =
    "Badger: $name, age: $age"
}

class Snake(
  var type: String,
  var length: Double
) {
  override fun toString() =
    "Snake: $type, length: $length"
}

class Moose(
  val age: Int,
  val height: Double
) {
  override fun toString() =
    "Moose, age: $age, height: $height"
}

fun main() {
  Badger("Bob", 11) eq "Badger: Bob, age: 11"
  Snake("Garden", 2.4) eq
    "Snake: Garden, length: 2.4"
  Moose(16, 7.2) eq
    "Moose, age: 16, height: 7.2"
}

Die Parameter id und years in Badger sind nur im Konstruktor-Körper verfügbar. Der Konstruktor-Körper besteht aus den Codezeilen, die keine Funktionsdefinitionen sind; in diesem Fall die Definitionen für name und age.

Oft möchte man, dass die Konstruktor-Parameter in Teilen der Klasse verfügbar sind, die nicht zum Konstruktor-Körper gehören, ohne dass man neue Bezeichner explizit definieren muss, wie wir es bei name und age getan haben. Wenn Sie Ihre Parameter als vars oder vals definieren, werden sie zu Eigenschaften und sind überall in der Klasse zugänglich. Sowohl Snake als auch Moose verwenden diesen Ansatz, und Sie können sehen, dass die Konstruktor-Parameter jetzt innerhalb ihrer jeweiligen toString()-Funktionen verfügbar sind.

Mit val deklarierte Konstruktor-Parameter können nicht geändert werden, aber die mit var deklarierten schon.

Wann immer Sie ein Objekt in einer Situation verwenden, die einen String erwartet, erzeugt Kotlin eine String-Darstellung dieses Objekts, indem es seine toString()-Mitgliedsfunktion aufruft. Um eine toString() zu definieren, müssen Sie ein neues Schlüsselwort verstehen: override. Dies ist notwendig (Kotlin besteht darauf), weil toString() bereits definiert ist. override teilt Kotlin mit, dass wir tatsächlich die Standard-toString() durch unsere eigene Definition ersetzen wollen. Die Explizität von override macht dies dem Leser klar und hilft, Fehler zu vermeiden.

Beachten Sie das Format der mehrzeiligen Parameterliste für Snake und Moose — dies ist der empfohlene Standard, wenn Sie zu viele Parameter haben, um sie in eine Zeile zu passen, sowohl für Konstruktoren als auch für Funktionen.

Einschränkung der Sichtbarkeit

Kotlin bietet Zugriffsmodifikatoren, die denen in anderen Sprachen wie C++ oder Java ähnlich sind. Diese ermöglichen es den Erstellern von Komponenten, zu entscheiden, was für den Client-Programmierer verfügbar ist. Zu den Zugriffsmodifikatoren von Kotlin gehören die Schlüsselwörter public, private, protected und internal. protected wird später erklärt.

Ein Zugriffsmodifikator wie public oder private erscheint vor der Definition einer Klasse, Funktion oder Eigenschaft. Jeder Zugriffsmodifikator steuert nur den Zugriff auf diese spezifische Definition.

Eine public-Definition ist für jeden verfügbar, insbesondere für den Client-Programmierer, der diese Komponente verwendet. Daher wirken sich alle Änderungen an einer public-Definition auf den Client-Code aus.

Wenn Sie keinen Modifikator angeben, ist Ihre Definition automatisch public. Aus Gründen der Klarheit geben Programmierer in bestimmten Fällen manchmal trotzdem redundant public an.

Wenn Sie eine Klasse, eine top-level Funktion oder Eigenschaft als private definieren, ist sie nur innerhalb dieser Datei verfügbar:

// Summary2/Boxes.kt
package summary2
import atomictest.*

private var count = 0                   // [1]

private class Box(val dimension: Int) { // [2]
  fun volume() =
    dimension * dimension * dimension
  override fun toString() =
    "Box volume: ${volume()}"
}

private fun countBox(box: Box) {        // [3]
  trace("$box")
  count++
}

fun countBoxes() {
  countBox(Box(4))
  countBox(Box(5))
}

fun main() {
  countBoxes()
  trace("$count boxes")
  trace eq """
    Box volume: 64
    Box volume: 125
    2 boxes
  """
}

Sie können auf private Eigenschaften ([1]), Klassen ([2]) und Funktionen ([3]) nur von anderen Funktionen und Klassen in der Datei Boxes.kt zugreifen. Kotlin verhindert, dass Sie auf private Top-Level-Elemente von einer anderen Datei aus zugreifen.

Klassenmitglieder können private sein:

// Summary2/JetPack.kt
package summary2
import atomictest.eq

class JetPack(
  private var fuel: Double   // [1]
) {
  private var warning = false
  private fun burn() =       // [2]
    if (fuel - 1 <= 0) {
      fuel = 0.0
      warning = true
    } else
      fuel -= 1
  public fun fly() = burn()  // [3]
  fun check() =              // [4]
    if (warning)             // [5]
      "Warning"
    else
      "OK"
}

fun main() {
  val jetPack = JetPack(3.0)
  while (jetPack.check() != "Warning") {
    jetPack.check() eq "OK"
    jetPack.fly()
  }
  jetPack.check() eq "Warning"
}
  • [1] fuel und warning sind beide private Eigenschaften und können nicht von Nicht-Mitgliedern von JetPack verwendet werden.
  • [2] burn() ist private und somit nur innerhalb von JetPack zugänglich.
  • [3] fly() und check() sind public und können überall verwendet werden.
  • [4] Kein Zugriffsmodifizierer bedeutet public Sichtbarkeit.
  • [5] Nur Mitglieder derselben Klasse können auf private Mitglieder zugreifen.

Da eine private Definition nicht für alle verfügbar ist, kann man sie im Allgemeinen ändern, ohne sich um den Client-Programmierer zu sorgen. Als Bibliotheksdesigner hält man normalerweise alles so private wie möglich und gibt nur Funktionen und Klassen frei, die Client-Programmierer verwenden sollen. Um die Größe und Komplexität der Beispielauflistungen in diesem Buch zu begrenzen, verwenden wir private nur in speziellen Fällen.

Jede Funktion, bei der Sie sicher sind, dass es sich nur um eine Hilfsfunktion handelt, kann private gemacht werden, um sicherzustellen, dass Sie sie nicht versehentlich anderswo verwenden und sich damit verbieten, die Funktion zu ändern oder zu entfernen.

Es kann nützlich sein, große Programme in Module zu unterteilen. Ein Modul ist ein logisch unabhängiger Teil einer Codebasis. Eine internal Definition ist nur innerhalb des Moduls zugänglich, in dem sie definiert ist. Die Art und Weise, wie Sie ein Projekt in Module unterteilen, hängt vom Build-System ab (wie Gradle oder Maven) und liegt außerhalb des Rahmens dieses Buches.

Module sind ein Konzept auf höherer Ebene, während Pakete eine feiner abgestufte Strukturierung ermöglichen.

Ausnahmen

Betrachten Sie toDouble(), das einen String in ein Double umwandelt. Was passiert, wenn Sie es für einen String aufrufen, der nicht in ein Double übersetzt wird?

// Summary2/ToDoubleException.kt

fun main() {
  // val i = "$1.9".toDouble()
}

Das Auskommentieren der Zeile in main() erzeugt eine Ausnahme. Hier ist die fehlerhafte Zeile auskommentiert, damit der Bau des Buches nicht gestoppt wird (der überprüft, ob jedes Beispiel wie erwartet kompiliert und ausgeführt wird).

Wenn eine Ausnahme ausgelöst wird, stoppt der aktuelle Ausführungspfad, und das Ausnahmeobjekt wird aus dem aktuellen Kontext herausgeschleudert. Wenn eine Ausnahme nicht abgefangen wird, bricht das Programm ab und zeigt einen Stack-Trace mit detaillierten Informationen an.

Um das Anzeigen von Ausnahmen durch Kommentieren und Auskommentieren von Code zu vermeiden, speichert atomictest.capture() die Ausnahme und vergleicht sie mit dem, was wir erwarten:

// Summary2/AtomicTestCapture.kt
import atomictest.*

fun main() {
  capture {
    "$1.9".toDouble()
  } eq "NumberFormatException: " +
    """For input string: "$1.9""""
}

capture() ist speziell für dieses Buch konzipiert, damit Sie die Ausnahme sehen und wissen, dass die Ausgabe vom Build-System des Buches überprüft wurde.

Eine weitere Strategie, wenn Ihre Funktion das erwartete Ergebnis nicht erfolgreich liefern kann, ist die Rückgabe von null. Später in Nullable Types diskutieren wir, wie null den Typ des resultierenden Ausdrucks beeinflusst.

Um eine Ausnahme zu werfen, verwenden Sie das Schlüsselwort throw, gefolgt von der Ausnahme, die Sie werfen möchten, zusammen mit allen Argumenten, die sie möglicherweise benötigt. quadraticZeroes() im folgenden Beispiel löst die quadratische Gleichung, die eine Parabel definiert:

ax2 + bx + c = 0

Die Lösung ist die quadratische Formel:

Die quadratische Formel
Die quadratische Formel

Das Beispiel findet die Nullstellen der Parabel, wo die Linien die x-Achse schneiden. Wir werfen Ausnahmen für zwei Einschränkungen:

  1. a darf nicht null sein.
  2. Damit Nullstellen existieren, darf b2 - 4ac nicht negativ sein.

Wenn Nullstellen existieren, gibt es zwei, daher erstellen wir die Roots-Klasse, um die Rückgabewerte zu halten:

// Summary2/Quadratic.kt
package summary2
import kotlin.math.sqrt
import atomictest.*

class Roots(
  val root1: Double,
  val root2: Double
)

fun quadraticZeroes(
  a: Double,
  b: Double,
  c: Double
): Roots {
  if (a == 0.0)
    throw IllegalArgumentException(
      "a is zero")
  val underRadical = b * b - 4 * a * c
  if (underRadical < 0)
    throw IllegalArgumentException(
      "Negative underRadical: $underRadical")
  val squareRoot = sqrt(underRadical)
  val root1 = (-b - squareRoot) / (2 * a)
  val root2 = (-b + squareRoot) / (2 * a)
  return Roots(root1, root2)
}

fun main() {
  capture {
    quadraticZeroes(0.0, 4.0, 5.0)
  } eq "IllegalArgumentException: " +
    "a is zero"
  capture {
    quadraticZeroes(3.0, 4.0, 5.0)
  } eq "IllegalArgumentException: " +
    "Negative underRadical: -44.0"
  val roots = quadraticZeroes(1.0, 2.0, -8.0)
  roots.root1 eq -4.0
  roots.root2 eq 2.0
}

Hier verwenden wir die Standard-Ausnahmeklasse IllegalArgumentException. Später werden Sie lernen, Ihre eigenen Ausnahmetypen zu definieren und sie spezifisch an Ihre Umstände anzupassen. Ihr Ziel ist es, die nützlichsten Nachrichten zu generieren, um die Unterstützung Ihrer Anwendung in der Zukunft zu vereinfachen.

Listen

Lists sind Kotlins grundlegender sequentieller Containertyp. Sie erstellen eine schreibgeschützte Liste mit listOf() und eine veränderbare Liste mit mutableListOf():

// Summary2/ReadonlyVsMutableList.kt
import atomictest.*

fun main() {
  val ints = listOf(5, 13, 9)
  // ints.add(11) // 'add()' not available
  for (i in ints) {
    if (i > 10) {
      trace(i)
    }
  }
  val chars = mutableListOf('a', 'b', 'c')
  chars.add('d') // 'add()' available
  chars += 'e'
  trace(chars)
  trace eq """
    13
    [a, b, c, d, e]
  """
}

Eine grundlegende Liste ist schreibgeschützt und enthält keine Änderungsfunktionen. Daher funktioniert die Änderungsfunktion add() nicht mit ints.

for Schleifen funktionieren gut mit Listen: for(i in ints) bedeutet, dass i jeden Wert in ints erhält.

chars wird als VeränderbareListe erstellt; sie kann mit Funktionen wie add() oder remove() modifiziert werden. Sie können auch += und -= verwenden, um Elemente hinzuzufügen oder zu entfernen.

Eine schreibgeschützte Liste ist nicht dasselbe wie eine unveränderliche Liste, die überhaupt nicht modifiziert werden kann. Hier weisen wir first, eine veränderbare Liste, second zu, einer schreibgeschützten Liste-Referenz. Die schreibgeschützte Eigenschaft von second verhindert nicht, dass sich die Liste über first ändert:

// Summary2/MultipleListReferences.kt
import atomictest.eq

fun main() {
  val first = mutableListOf(1)
  val second: List<Int> = first
  second eq listOf(1)
  first += 2
  // second sees the change:
  second eq listOf(1, 2)
}

first und second verweisen auf dasselbe Objekt im Speicher. Wir verändern die List über die first Referenz und beobachten dann diese Änderung in der second Referenz.

Hier ist eine List von Strings, die durch das Aufteilen eines dreifach-quotierten Absatzes erstellt wurde. Dies zeigt die Leistungsfähigkeit einiger Funktionen der Standardbibliothek. Beachten Sie, wie diese Funktionen verkettet werden können:

// Summary2/ListOfStrings.kt
import atomictest.*

fun main() {
  val wocky = """
    Twas brillig, and the slithy toves
      Did gyre and gimble in the wabe:
    All mimsy were the borogoves,
      And the mome raths outgrabe.
  """.trim().split(Regex("\\W+"))
  trace(wocky.take(5))
  trace(wocky.slice(6..12))
  trace(wocky.slice(6..18 step 2))
  trace(wocky.sorted().takeLast(5))
  trace(wocky.sorted().distinct().takeLast(5))
  trace eq """
    [Twas, brillig, and, the, slithy]
    [Did, gyre, and, gimble, in, the, wabe]
    [Did, and, in, wabe, mimsy, the, And]
    [the, the, toves, wabe, were]
    [slithy, the, toves, wabe, were]
  """
}

trim() erzeugt einen neuen String, bei dem die führenden und nachfolgenden Leerzeichen (einschließlich Zeilenumbrüche) entfernt wurden. split() teilt den String gemäß seinem Argument. In diesem Fall verwenden wir ein Regex-Objekt, das einen regulären Ausdruck erstellt—ein Muster, das die zu trennenden Teile abgleicht. \W ist ein spezielles Muster, das “kein Wortzeichen” bedeutet, und + bedeutet “eines oder mehrere der vorhergehenden”. Somit wird split() an einem oder mehreren Nicht-Wortzeichen brechen und somit den Textblock in seine einzelnen Wörter aufteilen.

In einem String-Literal steht \ vor einem speziellen Zeichen und erzeugt zum Beispiel ein Zeilenumbruchzeichen (\n) oder ein Tabulatorzeichen (\t). Um einen tatsächlichen \ im resultierenden String zu erzeugen, benötigen Sie zwei Backslashes: "\\". Daher erfordern alle regulären Ausdrücke einen zusätzlichen \, um einen Backslash einzufügen, es sei denn, Sie verwenden einen dreifach zitierten String: """\W+""".

take(n) erzeugt eine neue List, die die ersten n Elemente enthält. slice() erzeugt eine neue List, die die durch das Range-Argument ausgewählten Elemente enthält, und dieser Range kann einen step einschließen.

Beachten Sie den Namen sorted() anstelle von sort(). Wenn Sie sorted() aufrufen, erzeugt es eine sortierte List und lässt die ursprüngliche List unangetastet. sort() funktioniert nur mit einer MutableList, und diese Liste wird vor Ort sortiert—die ursprüngliche List wird verändert.

Wie der Name schon sagt, erzeugt takeLast(n) eine neue List der letzten n Elemente. An der Ausgabe können Sie sehen, dass “the” dupliziert ist. Dies wird durch Hinzufügen der distinct()-Funktion zur Aufrufkette beseitigt.

Parametrisierte Typen

Typparameter ermöglichen es uns, zusammengesetzte Typen zu beschreiben, am häufigsten Container. Insbesondere spezifizieren Typparameter, was ein Container enthält. Hier sagen wir Kotlin, dass numbers eine List von Int enthält, während strings eine List von String enthält:

// Summary2/ExplicitTyping.kt
package summary2
import atomictest.eq

fun main() {
  val numbers: List<Int> = listOf(1, 2, 3)
  val strings: List<String> =
    listOf("one", "two", "three")
  numbers eq "[1, 2, 3]"
  strings eq "[one, two, three]"
  toCharList("seven") eq "[s, e, v, e, n]"
}

fun toCharList(s: String): List<Char> =
  s.toList()

Sowohl bei den Definitionen von numbers als auch strings fügen wir Doppelpunkte und die Typdeklarationen List<Int> und List<String> hinzu. Die spitzen Klammern bezeichnen einen Typ-Parameter, der es uns ermöglicht zu sagen, “der Container enthält ‘Parameter’-Objekte.” Man spricht List<Int> typischerweise als “List von Int” aus.

Ein Rückgabewert kann ebenfalls einen Typ-Parameter haben, wie in toCharList() zu sehen ist. Man kann nicht einfach sagen, dass es eine List zurückgibt—Kotlin beschwert sich, also muss man den Typ-Parameter ebenfalls angeben.

Variable Argumentlisten

Das Schlüsselwort vararg steht für variable Argumentliste und erlaubt es einer Funktion, eine beliebige Anzahl von Argumenten (einschließlich null) des angegebenen Typs zu akzeptieren. Das vararg wird zu einem Array, das ähnlich wie eine List ist:

// Summary2/VarArgs.kt
package summary2
import atomictest.*

fun varargs(s: String, vararg ints: Int) {
  for (i in ints) {
    trace("$i")
  }
  trace(s)
}

fun main() {
  varargs("primes", 5, 7, 11, 13, 17, 19, 23)
  trace eq "5 7 11 13 17 19 23 primes"
}

Eine Funktionsdefinition kann nur einen Parameter als vararg spezifizieren. Jeder Parameter in der Liste kann das vararg sein, aber der letzte ist im Allgemeinen der einfachste.

Sie können ein Array von Elementen überall dort übergeben, wo ein vararg akzeptiert wird. Um ein Array zu erstellen, verwenden Sie arrayOf() auf die gleiche Weise wie listOf(). Ein Array ist immer veränderbar. Um ein Array in eine Sequenz von Argumenten (nicht nur ein einzelnes Element des Typs Array) zu konvertieren, verwenden Sie den Spread-Operator *:

// Summary2/ArraySpread.kt
import summary2.varargs
import atomictest.trace

fun main() {
  val array = intArrayOf(4, 5)      // [1]
  varargs("x", 1, 2, 3, *array, 6)  // [2]
  val list = listOf(9, 10, 11)
  varargs(
    "y", 7, 8, *list.toIntArray())  // [3]
  trace eq "1 2 3 4 5 6 x 7 8 9 10 11 y"
}

Wenn Sie ein Array von Primitivtypen wie im obigen Beispiel übergeben, muss die Array-Erstellungsfunktion spezifisch typisiert sein. Wenn [1] arrayOf(4, 5) anstelle von intArrayOf(4, 5) verwendet, erzeugt [2] einen Fehler: inferred type is Array<Int> but IntArray was expected.

Der Spread-Operator funktioniert nur mit Arrays. Wenn Sie eine List als Sequenz von Argumenten übergeben möchten, konvertieren Sie sie zuerst in ein Array und wenden Sie dann den Spread-Operator an, wie in [3]. Da das Ergebnis ein Array eines Primitivtyps ist, müssen wir die spezifische Konvertierungsfunktion toIntArray() verwenden.

Sets

Sets sind Sammlungen, die nur ein Element jedes Wertes zulassen. Ein Set verhindert automatisch Duplikate.

// Summary2/ColorSet.kt
package summary2
import atomictest.eq

val colors =
  "Yellow Green Green Blue"
    .split(Regex("""\W+""")).sorted()  // [1]

fun main() {
  colors eq
    listOf("Blue", "Green", "Green", "Yellow")
  val colorSet = colors.toSet()        // [2]
  colorSet eq
    setOf("Yellow", "Green", "Blue")
  (colorSet + colorSet) eq colorSet    // [3]
  val mSet = colorSet.toMutableSet()   // [4]
  mSet -= "Blue"
  mSet += "Red"                        // [5]
  mSet eq
    setOf("Yellow", "Green", "Red")
  // Set membership:
  ("Green" in colorSet) eq true        // [6]
  colorSet.contains("Red") eq false
}
  • [1] Der String wird mit einem regulären Ausdruck aufgeteilt (split()), wie zuvor für ListOfStrings.kt beschrieben.
  • [2] Wenn colors in das schreibgeschützte Set colorSet kopiert wird, wird einer der beiden "Green"-Strings entfernt, da es sich um ein Duplikat handelt.
  • [3] Hier erstellen und anzeigen wir ein neues Set mit dem +-Operator. Das Einfügen von doppelten Elementen in ein Set entfernt diese Duplikate automatisch.
  • [4] toMutableSet() erzeugt aus einem schreibgeschützten Set ein neues MutableSet.
  • [5] Für ein MutableSet fügen die Operatoren += und -= Elemente hinzu bzw. entfernen sie, wie sie es auch bei MutableLists tun.
  • [6] Testen Sie die Mitgliedschaft in einem Set mit in oder contains()

Die normalen mathematischen Mengenoperationen wie Vereinigung, Schnittmenge, Differenz usw. sind alle verfügbar.

Maps

Ein Map verbindet Schlüssel mit Werten und sucht einen Wert anhand eines Schlüssels. Sie erstellen ein Map, indem Sie Schlüssel-Wert-Paare zu mapOf() bereitstellen. Mit to trennen wir jeden Schlüssel von seinem zugehörigen Wert:

// Summary2/ASCIIMap.kt
import atomictest.eq

fun main() {
  val ascii = mapOf(
    "A" to 65,
    "B" to 66,
    "C" to 67,
    "I" to 73,
    "J" to 74,
    "K" to 75
  )
  ascii eq
    "{A=65, B=66, C=67, I=73, J=74, K=75}"
  ascii["B"] eq 66                   // [1]
  ascii.keys eq "[A, B, C, I, J, K]"
  ascii.values eq
    "[65, 66, 67, 73, 74, 75]"
  var kv = ""
  for (entry in ascii) {             // [2]
    kv += "${entry.key}:${entry.value},"
  }
  kv eq "A:65,B:66,C:67,I:73,J:74,K:75,"
  kv = ""
  for ((key, value) in ascii)        // [3]
    kv += "$key:$value,"
  kv eq "A:65,B:66,C:67,I:73,J:74,K:75,"
  val mutable = ascii.toMutableMap() // [4]
  mutable.remove("I")
  mutable eq
    "{A=65, B=66, C=67, J=74, K=75}"
  mutable.put("Z", 90)
  mutable eq
    "{A=65, B=66, C=67, J=74, K=75, Z=90}"
  mutable.clear()
  mutable["A"] = 100
  mutable eq "{A=100}"
}
  • [1] Ein Schlüssel ("B") wird verwendet, um mit dem [] Operator einen Wert nachzuschlagen. Sie können alle Schlüssel mit keys und alle Werte mit values erzeugen. Der Zugriff auf keys erzeugt ein Set, da alle Schlüssel in einer Map bereits eindeutig sein müssen (ansonsten hätten Sie Mehrdeutigkeiten bei einem Nachschlagen).
  • [2] Beim Iterieren durch eine Map entstehen Schlüssel-Wert-Paare als Map-Einträge.
  • [3] Sie können Schlüssel-Wert-Paare während des Iterierens entpacken.
  • [4] Sie können eine MutableMap aus einer Nur-Lese-Map mit toMutableMap() erstellen. Nun können wir Operationen durchführen, die mutable verändern, wie remove(), put(), und clear(). Eckige Klammern können ein neues Schlüssel-Wert-Paar in mutable zuweisen. Sie können auch ein Paar hinzufügen, indem Sie sagen map += key to value.

Eigenschafts-Accessoren

Der Zugriff auf die Eigenschaft i scheint unkompliziert:

// Summary2/PropertyReadWrite.kt
package summary2
import atomictest.eq

class Holder(var i: Int)

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

Allerdings ruft Kotlin Funktionen auf, um die Lese- und Schreiboperationen durchzuführen. Das Standardverhalten dieser Funktionen besteht darin, die in i gespeicherten Daten zu lesen und zu schreiben. Durch die Erstellung von Eigenschaftszugriffen ändern Sie die Aktionen, die beim Lesen und Schreiben auftreten.

Der Zugriff, der zum Abrufen des Werts einer Eigenschaft verwendet wird, wird als Getter bezeichnet. Um einen eigenen Getter zu erstellen, definieren Sie get() direkt nach der Eigenschaftsdeklaration. Der Zugriff, der zum Ändern einer veränderbaren Eigenschaft verwendet wird, wird als Setter bezeichnet. Um einen eigenen Setter zu erstellen, definieren Sie set() direkt nach der Eigenschaftsdeklaration. Die Reihenfolge der Definition von Gettern und Settern ist unwichtig, und Sie können einen ohne den anderen definieren.

Die Eigenschaftszugriffe im folgenden Beispiel imitieren die Standardimplementierungen und 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 die get()- und set()-Funktionen ein, um sie visuell mit der Eigenschaft zu verknüpfen, aber die tatsächliche Verknüpfung erfolgt, weil sie direkt nach dieser Eigenschaft definiert sind:

// Summary2/GetterAndSetter.kt
package summary2
import atomictest.*

class GetterAndSetter {
  var i: Int = 0
    get() {
      trace("get()")
      return field
    }
    set(value) {
      trace("set($value)")
      field = value
    }
}

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

Innerhalb des Getters und Setters wird der gespeicherte Wert indirekt mit dem field-Schlüsselwort manipuliert, das nur innerhalb dieser beiden Funktionen zugänglich ist. Es ist auch möglich, eine Eigenschaft zu erstellen, die kein field besitzt, sondern einfach den Getter aufruft, um ein Ergebnis zu erzeugen.

Wenn Sie eine private Eigenschaft deklarieren, werden beide Accessoren private. Sie können den Setter private und den Getter public machen. Das bedeutet, dass Sie die Eigenschaft außerhalb der Klasse lesen, aber ihren Wert nur innerhalb der Klasse ändern können.

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