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
tempeinvalist, modifizieren wir später dasTemperature-Objekt. Dieval-Definition verhindert, dass die Referenztempauf 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 inCellist einvar, sodass sie modifiziert werden kann. Die einfachen Anführungszeichen in der Initialisierung erzeugen einenChar-Typ, daher müssen alle Zuweisungen zuentryebenfallsChars sein. -
[2]
setValue()prüft, ob dieCellverfügbar ist und ob Sie das richtige Zeichen übergeben haben. Es gibt einString-Ergebnis zurück, um Erfolg oder Misserfolg anzuzeigen. -
[3]
play()überprüft, ob diex- undy-Argumente im Bereich liegen, und indiziert dann in die Matrix, wobei es sich auf die vonsetValue()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]
fuelundwarningsind beideprivateEigenschaften und können nicht von Nicht-Mitgliedern vonJetPackverwendet werden. -
[2]
burn()istprivateund somit nur innerhalb vonJetPackzugänglich. -
[3]
fly()undcheck()sindpublicund können überall verwendet werden. -
[4] Kein Zugriffsmodifizierer bedeutet
publicSichtbarkeit. -
[5] Nur Mitglieder derselben Klasse können auf
privateMitglieder 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:
Das Beispiel findet die Nullstellen der Parabel, wo die Linien die x-Achse schneiden. Wir werfen Ausnahmen für zwei Einschränkungen:
-
adarf nicht null sein. - 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
Stringwird mit einem regulären Ausdruck aufgeteilt (split()), wie zuvor fürListOfStrings.ktbeschrieben. -
[2] Wenn
colorsin das schreibgeschützteSet colorSetkopiert wird, wird einer der beiden"Green"-Strings entfernt, da es sich um ein Duplikat handelt. -
[3] Hier erstellen und anzeigen wir ein neues
Setmit dem+-Operator. Das Einfügen von doppelten Elementen in einSetentfernt diese Duplikate automatisch. -
[4]
toMutableSet()erzeugt aus einem schreibgeschütztenSetein neuesMutableSet. -
[5] Für ein
MutableSetfügen die Operatoren+=und-=Elemente hinzu bzw. entfernen sie, wie sie es auch beiMutableLists tun. -
[6] Testen Sie die Mitgliedschaft in einem
Setmitinodercontains()
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 mitkeysund alle Werte mitvalueserzeugen. Der Zugriff aufkeyserzeugt einSet, da alle Schlüssel in einerMapbereits eindeutig sein müssen (ansonsten hätten Sie Mehrdeutigkeiten bei einem Nachschlagen). -
[2] Beim Iterieren durch eine
Mapentstehen 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
MutableMapaus einer Nur-Lese-MapmittoMutableMap()erstellen. Nun können wir Operationen durchführen, diemutableverändern, wieremove(),put(), undclear(). Eckige Klammern können ein neues Schlüssel-Wert-Paar inmutablezuweisen. Sie können auch ein Paar hinzufügen, indem Sie sagenmap += 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.