Listen
Eine
Listist 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
Listverwendet eckige Klammern, um sich selbst darzustellen. -
[2]
for-Schleifen funktionieren gut mitLists:for(i in ints)bedeutet, dassijeden Wert inintserhält. Sie deklarierenval inicht und geben auch nicht seinen Typ an; Kotlin erkennt aus dem Kontext, dassieinfor-Schleifen-Identifikator ist. -
[3] Eckige Klammern indexieren in eine
List. EineListbehä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 Wert99ergibt. Somit ergibt ein Index von4den Wert11.
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]
list1bezieht sich auf ein veränderbares Objekt, das daher vor Ort modifiziert werden kann. Der Compiler übersetzt+=zum Aufruf vonplusAssign(). Es spielt keine Rolle, oblist1einvaloder einvarist, dalist1nach der Erstellung niemals neu zugewiesen wird—es verweist immer auf die gleiche veränderbare Liste. Wenn man es zu einemvarmacht, weist IntelliJ darauf hin, dass es sich nie ändert und schlägt vor, es zu einemvalzu machen. -
[2] Dies versucht, eine neue
Listzu erstellen, indemlist2und'B'kombiniert werden, aber es kann diese neueListnichtlist2neu zuweisen, dalist2einvalist. Ohne die Möglichkeit, diese Neuzuweisung durchzuführen, kann+=nicht kompiliert werden. -
[3] Erstellt
newListohne die bestehende unveränderlicheListzu modifizieren, auf dielist3verweist. -
[4] Da
list3einvarist, weist der CompilernewListzurück inlist3zu. Der vorherige Inhalt vonlist3wird dann vergessen, und es erscheint, als oblist3verändert wurde. Tatsächlich wurde das altelist3verworfen und durch das neu erstelltenewListersetzt, was die Illusion erzeugt, dasslist3verä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.