Julia bietet eine große Auswahl von Containertypen mit weitgehend ähnlichem Interface an.
Wir stellen hier `Tuple`, `Range` und `Dict` vor, im nächsten Kapitel dann `Array`, `Vector` und `Matrix`.
Diese Container sind:
- **iterierbar:** Man kann über die Elemente des Containers iterieren:
```julia
for x ∈ container ... end
```
- **indizierbar:** Man kann auf Elemente über ihren Index zugreifen:
```julia
x = container[i]
```
und einige sind auch
- **mutierbar**: Man kann Elemente hinzufügen, entfernen und ändern.
Weiterhin gibt es eine Reihe gemeinsamer Funktionen, z.B.
- `length(container)` --- Anzahl der Elemente
- `eltype(container)` --- Typ der Elemente
- `isempty(container)` --- Test, ob Container leer ist
- `empty!(container)` --- leert Container (nur wenn mutierbar)
## Tupeln
Ein Tupel ist ein nicht mutierbarer Container von Elementen. Es ist also nicht möglich, neue Elemente dazuzufügen oder den Wert eines Elements zu ändern.
```{julia}
t = (33, 4.5, "Hello")
@show t[2] # indizierbar
for i ∈ t println(i) end # iterierbar
```
Ein Tupel ist ein **inhomogener** Typ. Jedes Element hat seinen eigenen Typ und das zeigt sich auch im Typ des Tupels:
```{julia}
typeof(t)
```
Man verwendet Tupel gerne als Rückgabewerte von Funktionen, um mehr als ein Objekt zurückzulieferen.
```{julia}
# Ganzzahldivision und Rest:
# Quotient und Rest werden den Variablen q und r zugewiesen
q, r = divrem(71, 6)
@show q r;
```
Wie man hier sieht, kann man in bestimmten Konstrukten die Klammern auch weglassen.
Dieses *implict tuple packing/unpacking* verwendet man auch gerne in Mehrfachzuweisungen:
```{julia}
x, y, z = 12, 17, 203
```
```{julia}
y
```
Manche Funktionen bestehen auf Tupeln als Argument oder geben immer Tupeln zurück. Dann braucht man manchmal ein Tupel aus einem Element.
Das notiert man so:
```{julia}
x = (13,) # ein 1-Element-Tupel
```
Das Komma - und nicht die Klammern -- macht das Tupel.
```{julia}
x= (13) # kein Tupel
```
## Ranges
Wir haben *range*-Objekte schon in numerischen `for`-Schleifen verwendet.
```{julia}
r = 1:1000
typeof(r)
```
Es gibt verschiedene *range*-Typen. Wie man sieht, sind es über den Zahlentyp parametrisierte Typen und `UnitRange` ist z.B. ein *range* mit der Schrittweite 1. Ihre Konstruktoren heißen in der Regel `range()`.
Der Doppelpunkt ist eine spezielle Syntax.
- `a:b` wird vom Parser umgesetzt zu `range(a, b)`
- `a:b:c` wird umgesetzt zu `range(a, c, step=b)`
*Ranges* sind offensichtlich iterierbar, nicht mutierbar aber indizierbar.
```{julia}
(3:100)[20] # das zwanzigste Element
```
Wir erinnern an die Semantik der `for`-Schleife: `for i in 1:1000` heißt **nicht**:
- 'Die Schleifenvariable `i` wird bei jedem Durchlauf um eins erhöht' **sondern**
- 'Der Schleifenvariable werden nacheinander die Werte 1,2,3,...,1000 aus dem Container zugewiesen'.
Allerdings wäre es sehr ineffektiv, diesen Container tatsächlich explizit anzulegen.
- _Ranges_ sind "lazy" Vektoren, die nie wirklich irgendwo als konkrete Liste abgespeichert werden. Das macht sie als Iteratoren in `for`-Schleifen so nützlich: speichersparend und schnell.
- Sie sind 'Rezepte' oder Generatoren, die auf die Abfrage 'Gib mir dein nächstes Element!' antworten.
- Tatsächlich ist der Muttertyp `AbstractRange` ein Subtyp von `AbstractVector`.
Das Macro `@allocated` gibt aus, wieviel Bytes an Speicher bei der Auswertung eines Ausdrucks alloziert wurden.