Ein Tuple ist einem Vector (eindimensionales array) in vielerlei Hinsicht recht ähnlich. Es gibt jedoch ein paar wichtige Unterschiede.
( ) statt in eckigen Klammern [ ] geschrieben, wobei die Klammern weggelassen werden können, wenn die Bedeutung eindeutig ist.Unveränderlichkeit ermöglicht mehr Compiler-Optimierungen und macht Tupel performanter als Vektoren. Deshalb solltest du sie bevorzugen, wenn Veränderlichkeit nicht nötig ist.
julia> t = (3, 5.2, "xyz")
(3, 5.2, "xyz")
# Non-homogeneous:
julia> dump(t)
Tuple{Int64, Float64, String}
1: Int64 3
2: Float64 5.2
3: String "xyz"
julia> t[2]
5.2
# Immutable:
julia> t[2] = 7.3
ERROR: MethodError: no method matching setindex!(::Tuple{Int64, Float64, String}
Es gibt außerdem einen Tuple()-Konstruktor, der jede iterierbare Collection in ein Tupel umwandelt. Praktischerweise behandelt er einen String als iterierbare Collection von Zeichen.
julia> Tuple("Julia")
('J', 'u', 'l', 'i', 'a')
Bei jedem Tupel kannst du auf die Elemente über den Index zugreifen oder sie in einer Schleife durchlaufen.
Jedes rein numerische Tupel kann außerdem in mathematischen Funktionen wie sum() verwendet werden, genau wie Arrays.
Ein Tupel kann mit Hilfe der Mehrfachzuweisung in einzelne Elemente entpackt werden. Für Elemente, die du nicht brauchst, verwendest du den Unterstrich _ als Platzhaltervariable.
julia> tup = (1, "Julia", true)
(1, "Julia", true)
julia> num, name, _ = tup
(1, "Julia", true)
julia> uppercase(name), 2num
("JULIA", 2)
Um alle Elemente eines Tupels in ein anderes Tupel (oder allgemeiner in eine Collection) zu kopieren, gibt es einen splat-Operator ..., der es entpackt. Das wird im Konzept Funktionen ausführlicher behandelt.
julia> new_tup = (tup..., π, '😊')
(1, "Julia", true, π, '😊')
Ein Tuple enthält nur Werte. Ein NamedTuple ordnet jedem Wert einen Namen zu.
Auf einzelne Felder kannst du dann mit der „Punkt“-Notation zugreifen:
julia> nt = (a = 1, b = 7.3)
(a = 1, b = 7.3)
julia> dump(nt)
@NamedTuple{a::Int64, b::Float64}
a: Int64 1
b: Float64 7.3
julia> nt.b
7.3
Der Name wird intern als Symbol gespeichert (ein unveränderlicher Typ, und dem Namen wird ein Doppelpunkt : vorangestellt), daher gibt es eine alternative Syntax:
julia> nt[:b]
7.3
Manchmal ist es nützlich, die Erstellung eines benannten Tupels in zwei Schritte aufzuteilen:
# Step 1
julia> Student = NamedTuple{(:name, :grade), Tuple{String, Int}}
@NamedTuple{name::String, grade::Int64}
julia> typeof(Student)
DataType
# Step 2
julia> s = Student( ("Helen", 3) )
(name = "Helen", grade = 3)
julia> dump(s)
@NamedTuple{name::String, grade::Int64}
name: String "Helen"
grade: Int64 3
Es gibt überraschend viele andere Möglichkeiten, ein benanntes Tupel zu erstellen. Schau dir die Dokumentation an, wenn du dich dafür interessierst.
Es ist einfach, entweder nur die Namen (auch „Schlüssel“ genannt) oder nur die Werte zu erhalten:
julia> keys(s)
(:name, :grade)
julia> values(s)
("Helen", 3)
Beachte, dass eine Iteration über ein benanntes Tupel nur die Werte liefert und die Namen ignoriert werden.
Wenn die Namen bei einer Iteration ebenfalls nützlich sind, sind mehr Schritte nötig. Das NamedTuple kann in einen Vector aus Pairs umgewandelt werden (Pairs sind das Thema eines anderen Konzepts).
julia> pairs(s)
pairs(::NamedTuple) with 2 entries:
:name => "Helen"
:grade => 3
julia> collect(pairs(s))
2-element Vector{Pair{Symbol, Any}}:
:name => "Helen"
:grade => 3
Das benannte Tupel ist also ziemlich wörterbuchartig. Es unterscheidet sich jedoch in einigen wichtigen Punkten von einem [Dict][dict] in Julia:
Dict ist veränderlich; ein NamedTuple ist, wie jedes Tupel, unveränderlich.Dict ist flexibel beim Typ der Schlüssel, die es akzeptiert; ein NamedTuple kann nur Symbole als Schlüssel verwenden.Tupel (benannt oder nicht) sind eine sehr verbreitete Möglichkeit, strukturierte Daten an Funktionen zu übergeben und von ihnen zurückzubekommen. Unveränderlichkeit ist in diesem Anwendungsfall von Vorteil: Sie sorgt für Datensicherheit und ermöglicht aggressivere Compiler-Optimierungen.