Tupeltypen (C#-Referenz)

Das Feature Tupel ist in C# 7.0 und höher verfügbar und bietet eine überschaubare Syntax zum Gruppieren von mehreren Datenelementen in einer einfachen Datenstruktur. Im folgenden Beispiel wird veranschaulicht, wie Sie eine Tupelvariable deklarieren, initialisieren und dafür auf die zugehörigen Datenmember zugreifen können:

(double, int) t1 = (4.5, 3);
Console.WriteLine($"Tuple with elements {t1.Item1} and {t1.Item2}.");
// Output:
// Tuple with elements 4.5 and 3.

(double Sum, int Count) t2 = (4.5, 3);
Console.WriteLine($"Sum of {t2.Count} elements is {t2.Sum}.");
// Output:
// Sum of 3 elements is 4.5.

Wie im obigen Beispiel zu sehen ist, geben Sie zum Definieren eines Tupeltyps die Typen aller Datenmember und optional die Feldnamen an. Sie können keine Methoden in einem Tupeltyp definieren, aber Sie können die von .NET bereitgestellten Methoden verwenden. Dies wird im folgenden Beispiel veranschaulicht:

(double, int) t = (4.5, 3);
Console.WriteLine(t.ToString());
Console.WriteLine($"Hash code of {t} is {t.GetHashCode()}.");
// Output:
// (4.5, 3)
// Hash code of (4.5, 3) is 718460086.

Ab C# 7.3 unterstützen Tupletypen Gleichheitsoperatoren== und !=. Weitere Informationen finden Sie im Abschnitt Tupelgleichheit.

Tupeltypen sind Werttypen, und Tupelelemente sind öffentliche Felder. Bei Tupeln handelt es sich also um veränderliche Werttypen.

Hinweis

Für das Feature „Tupel“ werden der Typ System.ValueTuple und die zugehörigen generischen Typen (z. B. System.ValueTuple<T1,T2>) benötigt. Sie sind in .NET Core sowie in .NET Framework 4.7 und höher verfügbar. Fügen Sie dem Projekt das NuGet-Paket System.ValueTuple hinzu, wenn Sie Tupel in einem Projekt nutzen möchten, das auf .NET Framework 4.6.2 oder früher ausgerichtet ist.

Sie können Tupel mit einer beliebig großen Anzahl von Elementen definieren:

var t = 
(1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
11, 12, 13, 14, 15, 16, 17, 18,
19, 20, 21, 22, 23, 24, 25, 26);
Console.WriteLine(t.Item26);  // output: 26

Anwendungsfälle von Tupeln

Einer der häufigsten Anwendungsfälle für Tupel ist die Verwendung als Methodenrückgabetyp. Anstatt out-Methodenparameter zu definieren, können Sie also Methodenergebnisse in einem Tupelrückgabetyp gruppieren. Dies ist im folgenden Beispiel veranschaulicht:

var xs = new[] { 4, 7, 9 };
var limits = FindMinMax(xs);
Console.WriteLine($"Limits of [{string.Join(" ", xs)}] are {limits.min} and {limits.max}");
// Output:
// Limits of [4 7 9] are 4 and 9

var ys = new[] { -9, 0, 67, 100 };
var (minimum, maximum) = FindMinMax(ys);
Console.WriteLine($"Limits of [{string.Join(" ", ys)}] are {minimum} and {maximum}");
// Output:
// Limits of [-9 0 67 100] are -9 and 100

(int min, int max) FindMinMax(int[] input)
{
    if (input is null || input.Length == 0)
    {
        throw new ArgumentException("Cannot find minimum and maximum of a null or empty array.");
    }

    var min = int.MaxValue;
    var max = int.MinValue;
    foreach (var i in input)
    {
        if (i < min)
        {
            min = i;
        }
        if (i > max)
        {
            max = i;
        }
    }
    return (min, max);
}

Wie Sie im obigen Beispiel sehen, können Sie direkt mit der zurückgegebenen Tupelinstanz arbeiten oder sie in separate Variablen dekonstruieren.

Sie können Tupeltypen auch anstelle von anonymen Typen verwenden, z. B. in LINQ-Abfragen. Weitere Informationen finden Sie unter Auswählen zwischen anonymen Typen und Tupeltypen.

Normalerweise verwenden Sie Tupel, um lose zusammengehörende Datenelemente zu gruppieren. Dies ist normalerweise bei privaten und internen Hilfsmethoden hilfreich. Erwägen Sie bei Verwendung einer öffentlichen API, den Typ Klasse oder Struktur zu definieren.

Tupelfeldnamen

Sie können die Namen von Tupelfeldern entweder in einem Ausdruck für die Tupelinitialisierung oder in der Definition eines Tupeltyps explizit angeben. Dies wird im folgenden Beispiel veranschaulicht:

var t = (Sum: 4.5, Count: 3);
Console.WriteLine($"Sum of {t.Count} elements is {t.Sum}.");

(double Sum, int Count) d = (4.5, 3);
Console.WriteLine($"Sum of {d.Count} elements is {d.Sum}.");

Ab C# 7.1 gilt Folgendes: Wenn Sie keinen Feldnamen angeben, wird er ggf. vom Namen der entsprechenden Variablen in einem Ausdruck für die Tupelinitialisierung abgeleitet (wie im folgenden Beispiel):

var sum = 4.5;
var count = 3;
var t = (sum, count);
Console.WriteLine($"Sum of {t.count} elements is {t.sum}.");

Dies wird als Tupel-Projektionsinitialisierer bezeichnet. Der Name einer Variablen wird in den folgenden Fällen nicht auf einen Tupelfeldnamen projiziert:

  • Der Kandidatenname ist ein Membername eines Tupeltyps, z. B. Item3, ToString oder Rest.
  • Beim Namen des Kandidaten handelt es sich um das Duplikat des expliziten oder impliziten Feldnamens eines anderen Tupels.

In diesen Fällen geben Sie entweder explizit den Namen eines Felds an oder greifen über den Standardnamen auf ein Feld zu.

Die Standardnamen von Tupelfeldern lauten Item1, Item2, Item3 usw. Den Standardnamen eines Felds können Sie immer verwenden. Dies gilt auch, wenn ein Feldname explizit angegeben oder abgeleitet wird (wie im folgenden Beispiel):

var a = 1;
var t = (a, b: 2, 3);
Console.WriteLine($"The 1st element is {t.Item1} (same as {t.a}).");
Console.WriteLine($"The 2nd element is {t.Item2} (same as {t.b}).");
Console.WriteLine($"The 3rd element is {t.Item3}.");
// Output:
// The 1st element is 1 (same as 1).
// The 2nd element is 2 (same as 2).
// The 3rd element is 3.

Bei der Tupelzuweisung und bei Vergleichen der Tupelgleichheit werden Feldnamen nicht berücksichtigt.

Zur Kompilierzeit ersetzt der Compiler die nicht dem Standard entsprechenden Felder durch die jeweiligen Standardnamen. Daher sind explizit angegebene oder abgeleitete Feldnamen zur Laufzeit nicht verfügbar.

Tipp

Aktivieren Sie die .NET-Codeformatregel IDE0037 , um eine Einstellung für verzögerte oder explizite Tuplefeldnamen festzulegen.

Tupelzuweisung und -dekonstruktion

C# unterstützt die Zuweisung zwischen Tupeltypen, die die beiden folgenden Bedingungen erfüllen:

  • Beide Tupeltypen haben die gleiche Anzahl von Elementen.
  • Für jede Tupelposition ist der Typ des rechten Tupelelements mit dem Typ des entsprechenden linken Tupelelements identisch oder implizit konvertierbar.

Die Werte von Tupelelementen werden gemäß der Reihenfolge der Tupelelemente zugewiesen. Die Namen von Tupelfeldern werden ignoriert und nicht zugewiesen. Dies wird im folgenden Beispiel veranschaulicht:

(int, double) t1 = (17, 3.14);
(double First, double Second) t2 = (0.0, 1.0);
t2 = t1;
Console.WriteLine($"{nameof(t2)}: {t2.First} and {t2.Second}");
// Output:
// t2: 17 and 3.14

(double A, double B) t3 = (2.0, 3.0);
t3 = t2;
Console.WriteLine($"{nameof(t3)}: {t3.A} and {t3.B}");
// Output:
// t3: 17 and 3.14

Sie können auch den Zuweisungsoperator = verwenden, um eine Tupelinstanz in separate Variablen zu dekonstruieren. Hierfür können Sie eine der folgenden Vorgehensweisen wählen:

  • Explizites Deklarieren des Typs jeder Variablen in Klammern:

    var t = ("post office", 3.6);
    (string destination, double distance) = t;
    Console.WriteLine($"Distance to {destination} is {distance} kilometers.");
    // Output:
    // Distance to post office is 3.6 kilometers.
    
  • Verwenden des Schlüsselworts var außerhalb der Klammern, um implizit typisierte Variablen zu deklarieren und die Typen vom Compiler ableiten zu lassen:

    var t = ("post office", 3.6);
    var (destination, distance) = t;
    Console.WriteLine($"Distance to {destination} is {distance} kilometers.");
    // Output:
    // Distance to post office is 3.6 kilometers.
    
  • Verwenden von vorhandenen Variablen:

    var destination = string.Empty;
    var distance = 0.0;
    
    var t = ("post office", 3.6);
    (destination, distance) = t;
    Console.WriteLine($"Distance to {destination} is {distance} kilometers.");
    // Output:
    // Distance to post office is 3.6 kilometers.
    

Weitere Informationen zur Dekonstruktion von Tupeln und anderen Typen finden Sie unter Dekonstruieren von Tupeln und anderen Typen.

Tupelgleichheit

Ab C# 7.3 unterstützen Tupeltypen die Operatoren == und !=. Mit diesen Operatoren werden Member des linken Operanden gemäß der Reihenfolge der Tupelelemente mit den entsprechenden Membern des rechten Operanden verglichen.

(int a, byte b) left = (5, 10);
(long a, int b) right = (5, 10);
Console.WriteLine(left == right);  // output: True
Console.WriteLine(left != right);  // output: False

var t1 = (A: 5, B: 10);
var t2 = (B: 5, A: 10);
Console.WriteLine(t1 == t2);  // output: True
Console.WriteLine(t1 != t2);  // output: False

Wie im obigen Beispiel zu sehen ist, werden Tupelfeldnamen bei Operationen mit == und != nicht berücksichtigt.

Zwei Tupel sind vergleichbar, wenn die beiden folgenden Bedingungen erfüllt sind:

  • Beide Tupel weisen die gleiche Anzahl von Elementen auf. t1 != t2 wird beispielsweise nicht kompiliert, wenn t1 und t2 über eine unterschiedliche Anzahl von Elementen verfügen.
  • Für jede Tupelposition können die entsprechenden Elemente der linken und rechten Tupeloperanden mit den Operatoren == und != verglichen werden. (1, (2, 3)) == ((1, 2), 3) wird beispielsweise nicht kompiliert, weil 1 nicht mit (1, 2) vergleichbar ist.

Die Operatoren == und != vergleichen Tupel per „Kurzschluss“. Dies bedeutet, dass eine Operation sofort angehalten wird, wenn ein Paar mit ungleichen Elementen erkannt oder das Ende von Tupeln erreicht wird. Bevor ein Vergleich durchgeführt wird, werden aber alle Tupelelemente ausgewertet. Dies wird im folgenden Beispiel veranschaulicht:

Console.WriteLine((Display(1), Display(2)) == (Display(3), Display(4)));

int Display(int s)
{
    Console.WriteLine(s);
    return s;
}
// Output:
// 1
// 2
// 3
// 4
// False

Tupel als out-Parameter

Normalerweise gestalten Sie eine Methode, die out-Parameter enthält, in eine Methode um, die ein Tupel zurückgibt. Es gibt aber auch Fälle, in denen ein out-Parameter einen Tupeltyp aufweisen kann. Im folgenden Beispiel wird veranschaulicht, wie Sie Tupel als out-Parameter verwenden:

var limitsLookup = new Dictionary<int, (int Min, int Max)>()
{
    [2] = (4, 10),
    [4] = (10, 20),
    [6] = (0, 23)
};

if (limitsLookup.TryGetValue(4, out (int Min, int Max) limits))
{
    Console.WriteLine($"Found limits: min is {limits.Min}, max is {limits.Max}");
}
// Output:
// Found limits: min is 10, max is 20

Vergleich von Tupeln und System.Tuple

C#-Tupel, die auf System.ValueTuple-Typen basieren, unterscheiden sich von Tupeln, die auf System.Tuple-Typen basieren. Es gibt die folgenden Hauptunterschiede:

  • Bei System.ValueTuple-Typen handelt es sich um Werttypen. System.Tuple-Typen sind Verweistypen.
  • System.ValueTuple-Typen sind veränderlich. System.Tuple-Typen sind unveränderlich.
  • Datenmember von System.ValueTuple-Typen sind Felder. Datenmember von System.Tuple-Typen sind Eigenschaften.

C#-Sprachspezifikation

Weitere Informationen finden Sie in den folgenden Hinweisen zu Featurevorschlägen:

Siehe auch