Tupeltypen (C#-Referenz)

Das Tupel-Feature stellt eine kompakte Syntax zum Gruppieren mehrerer Datenelemente in einer einfachen Datenstruktur bereit. 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.

Tupeltypen unterstützen die 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.

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:

int[] xs = new int[] { 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

int[] ys = new int[] { -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.");
    }

    // Initialize min to MaxValue so every value in the input
    // is less than this initial value.
    var min = int.MaxValue;
    // Initialize max to MinValue so every value in the input
    // is greater than this initial value.
    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. Erwägen Sie bei öffentlichen APIs, den Typ als Klasse oder Struktur zu definieren.

Tupelfeldnamen

Sie geben die Namen von Tupelfeldern explizit in einem Ausdruck für die Tupelinitialisierung oder in der Definition eines Tupeltyps an. 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}.");

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 den obigen 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-Codestilregel IDE0037, um eine Voreinstellung für abgeleitete oder explizite Tupelfeldnamen festzulegen.

Ab C# 12 können Sie mit einer using-Anweisung einen Alias für einen Tupeltyp angeben. Im folgenden Beispiel wird ein global using-Alias für einen Tupeltyp mit zwei Integerwerten für einen zulässigen Min-Wert und Max-Wert hinzugefügt:

global using BandPass = (int Min, int Max);

Nach dem Deklarieren des Alias können Sie den BandPass-Namen als Alias für diesen Tupeltyp verwenden:

BandPass bracket = (40, 100);
Console.WriteLine($"The bandpass filter is {bracket.Min} to {bracket.Max}");

Ein Alias führt keinen neuen Typ ein, sondern erstellt nur ein Synonym für einen vorhandenen Typ. Sie können ein Tupel dekonstruieren, das mit dem BandPass-Alias deklariert wurde. Dies funktioniert genauso wie mit dem zugrunde liegenden Tupeltyp:

(int a , int b) = bracket;
Console.WriteLine($"The bracket is {a} to {b}");

Wie bei der Tupelzuweisung oder -dekonstruktion müssen die Namen der Tupelmember nicht übereinstimmen, da die Typen übereinstimmen.

Ebenso kann ein zweiter Alias mit derselben Stelligkeit und denselben Membertypen austauschbar mit dem ursprünglichen Alias verwendet werden. Sie können einen zweiten Alias deklarieren:

using Range = (int Minimum, int Maximum);

Sie können einem BandPass-Tupel ein Range-Tupel zuweisen. Wie bei allen Tupelzuweisungen müssen die Feldnamen nicht übereinstimmen, sondern nur die Typen und die Stelligkeit.

Range r = bracket;
Console.WriteLine($"The range is {r.Minimum} to {r.Maximum}");

Ein Alias für einen Tupeltyp bietet mehr semantische Informationen, wenn Sie Tupel verwenden. Es wird kein neuer Typ eingeführt. Um die Typsicherheit sicherzustellen, sollten Sie stattdessen einen Positions-record deklarieren.

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. Dazu gibt es verschiedene Möglichkeiten:

  • 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.
    
  • 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.
    
  • Deklarieren Sie einige Typen explizit und andere Typen implizit (mit var) innerhalb der Klammern:

    var t = ("post office", 3.6);
    (var destination, double 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.
    

Das Ziel eines Dekonstruktionsausdrucks kann sowohl vorhandene Variablen als auch Variablen enthalten, die in der Dekonstruktionsdeklaration deklariert wurden.

Sie können die Dekonstruktion auch mit einem Musterabgleich kombinieren, um die Eigenschaften von Feldern in einem Tupel zu untersuchen. Im folgenden Beispiel werden mehrere Integer durchlaufen und die durch 3 teilbaren ausgegeben. Es dekonstruiert das Tupelergebnis von Int32.DivRem und gleicht es mit einem Remainder von 0 ab:

for (int i = 4; i < 20;  i++)
{
    if (Math.DivRem(i, 3) is ( Quotient: var q, Remainder: 0 ))
    {
        Console.WriteLine($"{i} is divisible by 3, with quotient {q}");
    }
}

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

Tupelgleichheit

Tupeltypen unterstützen 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, da 1 nicht mit (1, 2) übereinstimmt.

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 unter

Weitere Informationen