Ein Tupel bündelt eine kleine, feste Anzahl von Werten zu einem Wert, ohne dafür eine Klasse zu deklarieren. ("Ana", 31) ist ein Tupel aus einem string und einem int. Tupel sind der idiomatische Weg, zwei oder drei Dinge aus einer Methode zurückzugeben und kurzlebige Paare innerhalb einer Methode zu halten.
Ein Tupel erzeugen
Auch der Typ wird mit Klammern geschrieben: (string, int) ist ein Tupeltyp, dessen erstes Element ein string und dessen zweites ein int ist.
Ausgabe:
Ana is 31
(3, 4)
179.70
(Ana, 32)
Ohne Namen heißen die Elemente Item1, Item2, Item3 und so weiter. Diese Tupel sind System.ValueTuple-Structs, eine Zuweisung an eine andere Variable kopiert also die Werte, und eine Änderung an der Kopie lässt das Original unberührt.
Benannte Elemente
Item1 sagt nichts darüber, was der Wert ist. Gib den Elementen Namen, entweder im Typ oder im Literal:
Ausgabe:
Ana, 31
Desk lamp costs 24.99
Ana
Ana
Namen sind eine Bequemlichkeit zur Kompilierzeit. Zur Laufzeit sind beide Tupel oben einfache ValueTuple<string, int>-Werte, deshalb kompiliert die Zuweisung von person an ein Tupel mit anderen Namen: Nur die Elementtypen und ihre Reihenfolge müssen passen.
Ab C# 7.1 werden Namen außerdem aus den Variablen im Literal abgeleitet, var t = (name, age); gibt dir also t.name und t.age, ohne sie zweimal zu schreiben. In C# 7.0 heißen diese Elemente nur Item1 und Item2.
Mehrere Werte aus einer Methode zurückgeben
Das ist der Hauptgrund, warum Tupel in die Sprache aufgenommen wurden. Davor gab es als Optionen out-Parameter, eine Einwegklasse oder System.Tuple mit seinem anonymen Item1.
Ausgabe:
Lowest 60, highest 95
Average 78.75
Die Elementnamen im Rückgabetyp werden zu den Namen, die der Aufrufer sieht. Vergleiche dieselbe Methode mit out-Parametern:
static void Stats(int[] scores, out int min, out int max, out double average) { ... }
Stats(scores, out int min, out int max, out double avg);
out-Parameter bleiben die Konvention für die Form von TryParse, bei der ein bool sagt, ob es geklappt hat, und der Wert daneben zurückkommt. Für „mehrere Dinge berechnen und zurückgeben“ liest sich ein Tupel besser. Siehe ref und out.
Dekonstruktion
Die Dekonstruktion packt ein Tupel in einer Anweisung in separate Variablen aus:
Ausgabe:
Lima 24.5
Lima
24.5
2 1
Die Variablennamen in var (city, temp) gehören dir; sie müssen nicht zu den Elementnamen des Tupels passen, weil die Dekonstruktion nach Position geht. Der Tausch (a, b) = (b, a) baut zuerst ein Tupel aus den alten Werten und weist dann zu, sodass keiner der Werte verloren geht.
Deine eigenen Klassen können Dekonstruktion unterstützen, indem sie eine Methode Deconstruct mit out-Parametern deklarieren. Ab C# 7 mit .NET Core 2.0 oder neuer hat auch KeyValuePair eine, eine Dictionary-Schleife kann also jeden Eintrag im Kopf auspacken:
class Point
{
public int X { get; }
public int Y { get; }
public Point(int x, int y) { X = x; Y = y; }
public void Deconstruct(out int x, out int y) { x = X; y = Y; }
}
var (x, y) = new Point(3, 4);
foreach (var (name, score) in scoresByName) // Dictionary<string, int>
{
Console.WriteLine($"{name}: {score}");
}
Dieselbe Form foreach (var (a, b) in listOfTuples) funktioniert für eine Liste von Tupeln. Das ganze Tupel als eine Schleifenvariable zu nehmen und seine benannten Elemente zu lesen, wie es der nächste Abschnitt tut, funktioniert genauso gut.
Tupel in Listen, LINQ und Dictionaries
Eine Liste benannter Tupel ist ein leichtgewichtiger Weg, Datenzeilen innerhalb einer Methode zu halten:
Ausgabe:
Chloe 95
Ana 88
Ben 72
(Ana, B), (Chloe, A)
Ben
False
ValueTuple implementiert Equals und GetHashCode Element für Element, (2, 7), an zwei verschiedenen Stellen gebaut, findet also denselben Dictionary-Eintrag. Das macht ein Tupel zum einfachsten zusammengesetzten Schlüssel: keine Klasse, keine handgeschriebene Hashfunktion, keine String-Verkettung wie row + ":" + col.
Gleichheit von Tupeln
Equals vergleicht Element für Element und funktioniert, seit es Tupel gibt:
Ausgabe:
True
False
C# 7.3 hat == und != für Tupel hinzugefügt, die zum selben elementweisen Vergleich kompilieren und ebenfalls Namen ignorieren:
if ((order.Status, order.Paid) == ("shipped", true)) { ... }
Console.WriteLine((1, "x") == (1, "x")); // True
System.Tuple gegenüber ValueTuple
.NET 4 hat System.Tuple eingeführt, eine Klasse, die mit Tuple.Create erzeugt wird. Sie ist noch im Framework und taucht in älterem Code und älteren APIs auf, weshalb Suchen nach „C# Tupel“ oft bei ihr landen.
ValueTuple (C# 7) | System.Tuple (.NET 4) | |
|---|---|---|
| Syntax | (1, "a"), (int, string) | Tuple.Create(1, "a"), Tuple<int, string> |
| Art | Struct (Werttyp) | Klasse (Referenztyp) |
| Elementnamen | Ja | Nein, nur Item1, Item2 |
| Veränderlich | Ja, Elemente sind Felder | Nein, schreibgeschützte Properties |
| Dekonstruktion | Ja | Ja, über Erweiterungsmethoden |
Ausgabe:
Ben 25
(Ben, 25)
ToValueTuple() und ToTuple() wandeln zwischen beiden um. Nimm in neuem Code lieber Werttupel: Sie allokieren nichts auf dem Heap und können Namen tragen.
Wann stattdessen eine Klasse oder ein Record
Tupel passen am besten, wenn die Gruppierung lokal und offensichtlich ist: Eine Methode gibt zwei Zahlen zurück, eine LINQ-Abfrage trägt ein Paar zum nächsten Schritt, ein Dictionary braucht einen zweiteiligen Schlüssel. Wechsle zu einem benannten Typ, wenn:
- Dieselbe Form in mehreren öffentlichen Methodensignaturen vorkommt.
(string, string, int)an fünf Stellen ist eine Klasse, die noch nicht geschrieben wurde. - Die Werte Verhalten, Validierung oder mehr als drei oder vier Elemente brauchen.
- Die Daten eine Grenze überschreiten, etwa bei der Serialisierung. Die Namen existieren zur Laufzeit nicht, Newtonsoft.Json schreibt also
Item1undItem2, undSystem.Text.Jsonschreibt{}, weil Tupelelemente Felder sind, die es standardmäßig überspringt.
Records aus C# 9 geben dir den Großteil der Bequemlichkeit eines Tupels mit einem echten Namen. record Score(string Name, int Points); hat Wertgleichheit, ein lesbares ToString und Dekonstruktion eingebaut. Siehe Records.
Häufige Fehler
- Erwarten, dass Namen zur Laufzeit erhalten bleiben. Reflection und Serializer sehen
Item1,Item2(oder gar nichts). - Eine Kopie verändern. Tupel sind Structs:
var t2 = t1; t2.Item1 = 5;lässtt1unverändert. - Tupel mit vielen Elementen. Ab drei oder vier liest sich ein benannter Typ besser.
- Die Reihenfolge bei der Dekonstruktion verwechseln. Sie geht nach Position: Bei einem Tupel
(Name, Age)legtvar (age, name) = person;den Namen inage, und weilvardie Typen ableitet, warnt dich nichts.
Häufig gestellte Fragen
Wie gebe ich in C# mehrere Werte aus einer Methode zurück?
Gib ein Tupel zurück: Deklariere den Rückgabetyp als (int Min, int Max) und schreibe return (lowest, highest);. Der Aufrufer liest result.Min und result.Max oder dekonstruiert mit var (min, max) = MinMax(data);. Die Alternativen sind out-Parameter, die zum TryParse-Muster passen, oder eine kleine Klasse oder ein Record, wenn das Ergebnis eine eigene Bedeutung hat.
Was ist ein benanntes Tupel in C#?
Ein Tupel, dessen Elemente Namen statt Item1, Item2 haben: (string Name, int Age) person = ("Ana", 31); erlaubt dir, person.Name zu schreiben. Die Namen existieren nur zur Kompilierzeit; darunter ist es weiterhin ein ValueTuple<string, int>, und Item1 funktioniert weiter.
Was ist der Unterschied zwischen Tuple und ValueTuple in C#?
System.Tuple (aus .NET 4) ist eine Klasse: auf dem Heap allokiert, unveränderlich, und seine Elemente heißen immer nur Item1, Item2. System.ValueTuple (C# 7) ist ein Struct mit veränderlichen Feldern, unterstützt Elementnamen und die Syntax (a, b) und ist das, was jedes Tupel-Literal erzeugt. Nimm in neuem Code Werttupel.
Wie funktioniert die Dekonstruktion von Tupeln in C#?
var (name, age) = person; deklariert zwei Variablen und weist ihnen die Elemente des Tupels der Reihe nach zu. Du kannst auch an bestehende Variablen zuweisen, mit (a, b) = (b, a), was sie tauscht, und Elemente mit dem Discard _ überspringen: var (_, age) = person;.
Kann ich Tupel in C# mit == vergleichen?
Seit C# 7.3 vergleichen == und != Tupel Element für Element: (1, "a") == (1, "a") ist true. Namen werden ignoriert; nur Positionen und Werte zählen. Vor C# 7.3 nimm Equals, das Werttupel auf dieselbe Weise implementieren.