Kotlin Bootcamp für Programmierer 5.1: Erweiterungen

Dieses Codelab ist Teil des Kotlin-Bootcamps für Programmierer. Sie können diesen Kurs am besten nutzen, wenn Sie die Codelabs der Reihe nach durcharbeiten. Je nach Ihrem Wissen können Sie einige Abschnitte möglicherweise überfliegen. Dieser Kurs richtet sich an Programmierer, die eine objektorientierte Sprache kennen und Kotlin lernen möchten.

Einführung

In diesem Codelab lernen Sie eine Reihe verschiedener nützlicher Funktionen in Kotlin kennen, darunter Paare, Sammlungen und Erweiterungsfunktionen.

In diesem Kurs wird keine einzelne Beispiel-App entwickelt. Stattdessen sollen die Lektionen Ihr Wissen erweitern, sind aber weitgehend unabhängig voneinander, sodass Sie Abschnitte, mit denen Sie vertraut sind, überspringen können. Um die Beispiele zu veranschaulichen, wird in vielen ein Aquarium verwendet. Wenn Sie die ganze Geschichte des Aquariums sehen möchten, können Sie sich den Udacity-Kurs Kotlin Bootcamp for Programmers ansehen.

Was Sie bereits wissen sollten

  • Die Syntax von Kotlin-Funktionen, -Klassen und -Methoden
  • So arbeiten Sie mit der REPL (Read-Eval-Print Loop) von Kotlin in IntelliJ IDEA
  • Neue Klasse in IntelliJ IDEA erstellen und Programm ausführen

Lerninhalte

  • Mit Paaren und Dreiergruppen arbeiten
  • Weitere Informationen zu Sammlungen
  • Konstanten definieren und verwenden
  • Erweiterungsfunktionen schreiben

Aufgaben

  • Paare, Triple und Hashmaps in der REPL
  • Verschiedene Möglichkeiten zum Organisieren von Konstanten
  • Erweiterungsfunktion und ‑property schreiben

In dieser Aufgabe lernen Sie Paare und Tupel kennen und erfahren, wie Sie sie entpacken. Paare und Tupel sind vorgefertigte Datenklassen für 2 oder 3 generische Elemente. Das kann beispielsweise nützlich sein, wenn eine Funktion mehr als einen Wert zurückgeben soll.

Angenommen, Sie haben eine List von Fischen und eine Funktion isFreshWater(), mit der geprüft wird, ob es sich um einen Süß- oder Salzwasserfisch handelt. List.partition() gibt zwei Listen zurück: eine mit den Elementen, für die die Bedingung true ist, und eine mit den Elementen, für die die Bedingung false ist.

val twoLists = fish.partition { isFreshWater(it) }
println("freshwater: ${twoLists.first}")
println("saltwater: ${twoLists.second}")

Schritt 1: Paare und Dreiergruppen bilden

  1. Öffnen Sie die REPL (Tools > Kotlin > Kotlin REPL).
  2. Erstellen Sie ein Paar, das ein Gerät mit dem Zweck seiner Verwendung verknüpft, und geben Sie dann die Werte aus. Sie können ein Paar erstellen, indem Sie einen Ausdruck erstellen, der zwei Werte, z. B. zwei Strings, mit dem Keyword to verbindet. Anschließend können Sie mit .first oder .second auf die einzelnen Werte verweisen.
val equipment = "fish net" to "catching fish"
println("${equipment.first} used for ${equipment.second}")
⇒ fish net used for catching fish
  1. Erstellen Sie ein Tupel und geben Sie es mit toString() aus. Wandeln Sie es dann mit toList() in eine Liste um. Sie erstellen ein Triple mit Triple() mit drei Werten. Verwenden Sie .first, .second und .third, um auf die einzelnen Werte zu verweisen.
val numbers = Triple(6, 9, 42)
println(numbers.toString())
println(numbers.toList())
⇒ (6, 9, 42)
[6, 9, 42]

In den obigen Beispielen wird für alle Teile des Paares oder Tripletts derselbe Typ verwendet. Das ist jedoch nicht erforderlich. Die Teile können beispielsweise ein String, eine Zahl oder eine Liste sein – oder auch ein weiteres Paar oder Triple.

  1. Erstellen Sie ein Paar, bei dem der erste Teil des Paares selbst ein Paar ist.
val equipment2 = ("fish net" to "catching fish") to "equipment"
println("${equipment2.first} is ${equipment2.second}\n")
println("${equipment2.first.second}")
⇒ (fish net, catching fish) is equipment
⇒ catching fish

Schritt 2: Einige Paare und Tupel entstrukturieren

Das Aufteilen von Paaren und Tripeln in ihre Bestandteile wird als Destrukturierung bezeichnet. Weisen Sie das Paar oder das Triple der entsprechenden Anzahl von Variablen zu. Kotlin weist den Wert jedes Teils in der richtigen Reihenfolge zu.

  1. Entpacken Sie ein Paar und geben Sie die Werte aus.
val equipment = "fish net" to "catching fish"
val (tool, use) = equipment
println("$tool is used for $use")
⇒ fish net is used for catching fish
  1. Ein Tupel entpacken und die Werte ausgeben
val numbers = Triple(6, 9, 42)
val (n1, n2, n3) = numbers
println("$n1 $n2 $n3")
⇒ 6 9 42

Das Destrukturieren von Paaren und Tripeln funktioniert genauso wie bei Datenklassen, was in einem früheren Codelab behandelt wurde.

In dieser Aufgabe erfahren Sie mehr über Sammlungen, einschließlich Listen, und einen neuen Sammlungstyp, Hashmaps.

Schritt 1: Weitere Informationen zu Listen

  1. Listen und veränderliche Listen wurden in einer früheren Lektion eingeführt. Sie sind eine sehr nützliche Datenstruktur, daher bietet Kotlin eine Reihe von integrierten Funktionen für Listen. Hier finden Sie eine Teilliste der Funktionen für Listen. Vollständige Listen finden Sie in der Kotlin-Dokumentation für List und MutableList.

Funktion

Purpose

add(element: E)

Fügt der veränderlichen Liste ein Element hinzu.

remove(element: E)

Element aus einer veränderlichen Liste entfernen

reversed()

Gibt eine Kopie der Liste mit den Elementen in umgekehrter Reihenfolge zurück.

contains(element: E)

Gibt true zurück, wenn die Liste das Element enthält.

subList(fromIndex: Int, toIndex: Int)

Gibt einen Teil der Liste zurück, vom ersten Index bis zum zweiten Index (ausschließlich).

  1. Erstellen Sie im REPL eine Liste mit Zahlen und rufen Sie sum() dafür auf. Damit werden alle Elemente summiert.
val list = listOf(1, 5, 3, 4)
println(list.sum())
⇒ 13
  1. Erstellen Sie eine Liste mit Strings und summieren Sie die Liste.
val list2 = listOf("a", "bbb", "cc")
println(list2.sum())
⇒ error: none of the following functions can be called with the arguments supplied:
  1. Wenn das Element nicht direkt von List summiert werden kann, z. B. ein String, können Sie mit .sumBy() und einer Lambda-Funktion angeben, wie es summiert werden soll, z. B. nach der Länge jedes Strings. Der Standardname für ein Lambda-Argument ist it. Hier bezieht sich it auf jedes Element der Liste, während die Liste durchlaufen wird.
val list2 = listOf("a", "bbb", "cc")
println(list2.sumBy { it.length })
⇒ 6
  1. Sie können noch viel mehr mit Listen machen. Eine Möglichkeit, die verfügbaren Funktionen zu sehen, besteht darin, in IntelliJ IDEA eine Liste zu erstellen, den Punkt hinzuzufügen und dann die Liste der automatischen Vervollständigung in der Kurzinfo anzusehen. Das funktioniert für jedes Objekt. Probieren Sie es mit einer Liste aus.

  1. Wählen Sie listIterator() aus der Liste aus und durchlaufen Sie dann die Liste mit einer for-Anweisung. Geben Sie alle Elemente durch Leerzeichen getrennt aus.
val list2 = listOf("a", "bbb", "cc")
for (s in list2.listIterator()) {
    println("$s ")
}
⇒ a bbb cc

Schritt 2: Hashmaps ausprobieren

In Kotlin können Sie mit hashMapOf() so gut wie alles auf alles andere abbilden. Hashmaps sind wie eine Liste von Paaren, wobei der erste Wert als Schlüssel dient.

  1. Erstelle eine Hashmap, die Symptome (die Schlüssel) und Krankheiten von Fischen (die Werte) zuordnet.
val cures = hashMapOf("white spots" to "Ich", "red sores" to "hole disease")
  1. Sie können den Wert der Krankheit dann anhand des Symptom-Schlüssels mit get() oder noch kürzer mit eckigen Klammern [] abrufen.
println(cures.get("white spots"))
⇒ Ich
println(cures["red sores"])
⇒ hole disease
  1. Geben Sie ein Symptom an, das nicht auf der Karte enthalten ist.
println(cures["scale loss"])
⇒ null

Wenn ein Schlüssel nicht in der Karte enthalten ist, wird beim Versuch, die entsprechende Krankheit zurückzugeben, null zurückgegeben. Je nach Kartendaten ist es möglich, dass kein Schlüssel gefunden wird. Für solche Fälle bietet Kotlin die Funktion getOrDefault().

  1. Versuchen Sie, mit getOrDefault() nach einem Schlüssel zu suchen, der keine Übereinstimmung hat.
println(cures.getOrDefault("bloating", "sorry, I don't know"))
⇒ sorry, I don't know

Wenn Sie mehr als nur einen Wert zurückgeben müssen, bietet Kotlin die Funktion getOrElse().

  1. Ändern Sie den Code so, dass getOrElse() anstelle von getOrDefault() verwendet wird.
println(cures.getOrElse("bloating") {"No cure for this"})
⇒ No cure for this

Anstatt einen einfachen Standardwert zurückzugeben, wird der Code zwischen den geschweiften Klammern {} ausgeführt. Im Beispiel gibt else einfach einen String zurück. Es könnte aber auch eine Webseite mit einer Heilungsmethode gefunden und zurückgegeben werden.

Genau wie bei mutableListOf können Sie auch eine mutableMapOf erstellen. In einer veränderlichen Karte können Sie Elemente einfügen und entfernen. „Veränderlich“ bedeutet, dass sich etwas ändern kann, „unveränderlich“ bedeutet, dass sich etwas nicht ändern kann.

  1. Erstellen Sie eine Inventarkarte, die geändert werden kann, und ordnen Sie einen Ausrüstungsstring der Anzahl der Artikel zu. Erstelle es mit einem Fischernetz darin, füge dann 3 Aquarienreiniger mit put() zum Inventar hinzu und entferne das Fischernetz mit remove().
val inventory = mutableMapOf("fish net" to 1)
inventory.put("tank scrubber", 3)
println(inventory.toString())
inventory.remove("fish net")
println(inventory.toString())
⇒ {fish net=1, tank scrubber=3}{tank scrubber=3}

In dieser Aufgabe lernen Sie Konstanten in Kotlin und verschiedene Möglichkeiten kennen, sie zu organisieren.

Schritt 1: Informationen zu „const“ und „val“

  1. Versuchen Sie, in der REPL eine numerische Konstante zu erstellen. In Kotlin können Sie Konstanten auf oberster Ebene erstellen und ihnen zur Kompilierzeit mit const val einen Wert zuweisen.
const val rocks = 3

Der Wert wird zugewiesen und kann nicht geändert werden. Das ähnelt sehr der Deklaration einer regulären val. Was ist also der Unterschied zwischen const val und val? Der Wert für const val wird zur Kompilierzeit festgelegt, während der Wert für val während der Programmausführung festgelegt wird. Das bedeutet, dass val zur Laufzeit von einer Funktion zugewiesen werden kann.

Das bedeutet, dass val ein Wert aus einer Funktion zugewiesen werden kann, const val jedoch nicht.

val value1 = complexFunctionCall() // OK
const val CONSTANT1 = complexFunctionCall() // NOT ok

Außerdem funktioniert const val nur auf der obersten Ebene und in Singleton-Klassen, die mit object deklariert wurden, nicht mit regulären Klassen. So können Sie eine Datei oder ein Singleton-Objekt erstellen, das nur Konstanten enthält, und diese nach Bedarf importieren.

object Constants {
    const val CONSTANT2 = "object constant"
}
val foo = Constants.CONSTANT2

Schritt 2: Companion-Objekt erstellen

In Kotlin gibt es kein Konzept für Konstanten auf Klassenebene.

Wenn Sie Konstanten innerhalb einer Klasse definieren möchten, müssen Sie sie in Companion-Objekte einfügen, die mit dem Keyword companion deklariert werden. Das Companion-Objekt ist im Grunde ein Singleton-Objekt innerhalb der Klasse.

  1. Erstellen Sie eine Klasse mit einem Begleitobjekt, das eine Stringkonstante enthält.
class MyClass {
    companion object {
        const val CONSTANT3 = "constant in companion"
    }
}

Der grundlegende Unterschied zwischen Companion-Objekten und regulären Objekten ist:

  • Companion-Objekte werden aus dem statischen Konstruktor der enthaltenden Klasse initialisiert. Sie werden also erstellt, wenn das Objekt erstellt wird.
  • Reguläre Objekte werden bei der ersten Verwendung initialisiert.

Es gibt noch mehr, aber alles, was Sie jetzt wissen müssen, ist, dass Konstanten in Klassen in einem Companion-Objekt eingeschlossen werden.

In dieser Aufgabe erfahren Sie, wie Sie das Verhalten von Klassen erweitern. Es ist sehr üblich, Hilfsfunktionen zu schreiben, um das Verhalten einer Klasse zu erweitern. Kotlin bietet eine praktische Syntax zum Deklarieren dieser Hilfsfunktionen: Erweiterungsfunktionen.

Mit Erweiterungsfunktionen können Sie einer vorhandenen Klasse Funktionen hinzufügen, ohne auf ihren Quellcode zugreifen zu müssen. Sie können sie beispielsweise in einer Extensions.kt-Datei deklarieren, die Teil Ihres Pakets ist. Dadurch wird die Klasse nicht wirklich geändert, aber Sie können die Punktnotation verwenden, wenn Sie die Funktion für Objekte dieser Klasse aufrufen.

Schritt 1: Erweiterungsfunktion schreiben

  1. Schreiben Sie in der REPL eine einfache Erweiterungsfunktion, hasSpaces(), um zu prüfen, ob ein String Leerzeichen enthält. Dem Funktionsnamen wird die Klasse vorangestellt, auf die er sich bezieht. Innerhalb der Funktion verweist this auf das Objekt, für das sie aufgerufen wird, und it auf den Iterator im find()-Aufruf.
fun String.hasSpaces(): Boolean {
    val found = this.find { it == ' ' }
    return found != null
}
println("Does it have spaces?".hasSpaces())
⇒ true
  1. Sie können die Funktion hasSpaces() vereinfachen. Die this ist nicht explizit erforderlich. Die Funktion kann auf einen einzelnen Ausdruck reduziert und zurückgegeben werden. Daher sind auch die geschweiften Klammern {} nicht erforderlich.
fun String.hasSpaces() = find { it == ' ' } != null

Schritt 2: Einschränkungen von Erweiterungen kennenlernen

Erweiterungsfunktionen haben nur Zugriff auf die öffentliche API der Klasse, die sie erweitern. Auf Variablen, die private sind, kann nicht zugegriffen werden.

  1. Fügen Sie einer Property, die mit private gekennzeichnet ist, Erweiterungsfunktionen hinzu.
class AquariumPlant(val color: String, private val size: Int)

fun AquariumPlant.isRed() = color == "red"    // OK
fun AquariumPlant.isBig() = size > 50         // gives error
⇒ error: cannot access 'size': it is private in 'AquariumPlant'
  1. Sehen Sie sich den Code unten an und überlegen Sie, was ausgegeben wird.
open class AquariumPlant(val color: String, private val size: Int)

class GreenLeafyPlant(size: Int) : AquariumPlant("green", size)

fun AquariumPlant.print() = println("AquariumPlant")
fun GreenLeafyPlant.print() = println("GreenLeafyPlant")

val plant = GreenLeafyPlant(size = 10)
plant.print()
println("\n")
val aquariumPlant: AquariumPlant = plant
aquariumPlant.print()  // what will it print?
⇒ GreenLeafyPlant
AquariumPlant

plant.print() Abzüge GreenLeafyPlant Sie würden vielleicht erwarten, dass aquariumPlant.print() auch GreenLeafyPlant ausgibt, da ihr der Wert von plant zugewiesen wurde. Der Typ wird jedoch zur Kompilierzeit aufgelöst, sodass AquariumPlant ausgegeben wird.

Schritt 3: Erweiterungseigenschaft hinzufügen

Neben Erweiterungsfunktionen können Sie in Kotlin auch Erweiterungsproperties hinzufügen. Wie bei Erweiterungsfunktionen geben Sie die Klasse an, die Sie erweitern, gefolgt von einem Punkt und dann dem Namen der Eigenschaft.

  1. Fügen Sie im REPL der Erweiterung AquariumPlant die Property isGreen hinzu, die true ist, wenn die Farbe grün ist.
val AquariumPlant.isGreen: Boolean
   get() = color == "green"

Auf das Attribut isGreen kann wie auf ein reguläres Attribut zugegriffen werden. Beim Zugriff wird der Getter für isGreen aufgerufen, um den Wert abzurufen.

  1. Geben Sie das Attribut isGreen für die Variable aquariumPlant aus und sehen Sie sich das Ergebnis an.
aquariumPlant.isGreen
⇒ res4: kotlin.Boolean = true

Schritt 4: Informationen zu Nullable-Empfängern

Die Klasse, die Sie erweitern, wird als Empfänger bezeichnet. Es ist möglich, diese Klasse auf „nullable“ zu setzen. In diesem Fall kann die im Textkörper verwendete Variable this null sein. Testen Sie das. Sie sollten einen nullable-Empfänger verwenden, wenn Sie erwarten, dass Aufrufer Ihre Erweiterungsmethode für nullable-Variablen aufrufen möchten, oder wenn Sie ein Standardverhalten bereitstellen möchten, wenn Ihre Funktion auf null angewendet wird.

  1. Definieren Sie weiterhin im REPL eine pull()-Methode, die einen Nullable-Empfänger akzeptiert. Dies wird durch ein Fragezeichen ? nach dem Typ und vor dem Punkt angegeben. Im Text können Sie mit „questionmark-dot-apply“ ?.apply. prüfen, ob this nicht null ist.
fun AquariumPlant?.pull() {
   this?.apply {
       println("removing $this")
   }
}

val plant: AquariumPlant? = null
plant.pull()
  1. In diesem Fall erfolgt keine Ausgabe, wenn Sie das Programm ausführen. Da plant null ist, wird das innere println() nicht aufgerufen.

Erweiterungsfunktionen sind sehr leistungsstark und der Großteil der Kotlin-Standardbibliothek ist als Erweiterungsfunktionen implementiert.

In dieser Lektion haben Sie mehr über Sammlungen und Konstanten erfahren und einen Einblick in die Leistungsfähigkeit von Erweiterungsfunktionen und ‑attributen erhalten.

  • Mit Paaren und Tripletts können mehrere Werte aus einer Funktion zurückgegeben werden. Beispiel:
    val twoLists = fish.partition { isFreshWater(it) }
  • Kotlin bietet viele nützliche Funktionen für List, z. B. reversed(), contains() und subList().
  • Mit einer HashMap können Schlüssel Werten zugeordnet werden. Beispiel:
    val cures = hashMapOf("white spots" to "Ich", "red sores" to "hole disease")
  • Deklarieren Sie Kompilierzeitkonstanten mit dem Keyword const. Sie können sie auf der obersten Ebene platzieren, in einem Singleton-Objekt organisieren oder in ein Companion-Objekt einfügen.
  • Ein Companion-Objekt ist ein Singleton-Objekt innerhalb einer Klassendefinition, das mit dem Keyword companion definiert wird.
  • Mit Erweiterungsfunktionen und -attributen können Sie einer Klasse Funktionen hinzufügen. Beispiel:
    fun String.hasSpaces() = find { it == ' ' } != null
  • Mit einem nullable Receiver können Sie Erweiterungen für eine Klasse erstellen, die null sein können. Der Operator ?. kann mit apply kombiniert werden, um vor der Ausführung von Code nach null zu suchen. Beispiel:
    this?.apply { println("removing $this") }

Kotlin-Dokumentation

Wenn Sie weitere Informationen zu einem Thema in diesem Kurs benötigen oder nicht weiterkommen, ist https://kotlinlang.org der beste Ausgangspunkt.

Kotlin-Tutorials

Die Website https://try.kotlinlang.org enthält umfangreiche Tutorials namens „Kotlin Koans“, einen webbasierten Interpreter und eine vollständige Referenzdokumentation mit Beispielen.

Udacity-Kurs

Den Udacity-Kurs zu diesem Thema finden Sie unter Kotlin Bootcamp for Programmers.

IntelliJ IDEA

Dokumentation für IntelliJ IDEA finden Sie auf der JetBrains-Website.

In diesem Abschnitt werden mögliche Hausaufgaben für Schüler und Studenten aufgeführt, die dieses Codelab im Rahmen eines von einem Kursleiter geleiteten Kurses durcharbeiten. Es liegt in der Verantwortung des Kursleiters, Folgendes zu tun:

  • Weisen Sie bei Bedarf Aufgaben zu.
  • Teilen Sie den Schülern/Studenten mit, wie sie Hausaufgaben abgeben können.
  • Benoten Sie die Hausaufgaben.

Lehrkräfte können diese Vorschläge nach Belieben nutzen und auch andere Hausaufgaben zuweisen, die sie für angemessen halten.

Wenn Sie dieses Codelab selbst durcharbeiten, können Sie mit diesen Hausaufgaben Ihr Wissen testen.

Beantworten Sie diese Fragen

Frage 1

Welche der folgenden Optionen gibt eine Kopie einer Liste zurück?

▢ add()

▢ remove()

▢ reversed()

▢ contains()

Frage 2

Welche dieser Erweiterungsfunktionen für class AquariumPlant(val color: String, val size: Int, private val cost: Double, val leafy: Boolean) führt zu einem Compilerfehler?

▢ fun AquariumPlant.isRed() = color == "red"

▢ fun AquariumPlant.isBig() = size > 45

▢ fun AquariumPlant.isExpensive() = cost > 10.00

▢ fun AquariumPlant.isNotLeafy() = leafy == false

Frage 3

An welchem der folgenden Orte können Sie keine Konstanten mit const val definieren?

▢ auf der obersten Ebene einer Datei

▢ in regulären Kursen

▢ in Singleton-Objekten

▢ in Companion-Objekten

Fahren Sie mit der nächsten Lektion fort: 5.2 Generics

Eine Übersicht über den Kurs mit Links zu anderen Codelabs finden Sie unter „Kotlin Bootcamp for Programmers: Welcome to the course“.