Krotka (tuple) łączy małą, stałą liczbę wartości w jedną wartość bez deklarowania dla niej klasy. ("Ana", 31) to krotka ze string i int. Krotki to idiomatyczny sposób na zwrócenie dwóch lub trzech rzeczy z metody i na przechowywanie krótko żyjących par wewnątrz metody.
Tworzenie krotki
Typ też zapisuje się w nawiasach: (string, int) to typ krotki, której pierwszy element to string, a drugi int.
Wynik:
Ana is 31
(3, 4)
179.70
(Ana, 32)
Bez nazw elementy to Item1, Item2, Item3 i tak dalej. Te krotki to struktury System.ValueTuple, więc przypisanie jednej do innej zmiennej kopiuje wartości, a zmiana kopii nie wpływa na oryginał.
Nazwane elementy
Item1 nic nie mówi o tym, czym jest wartość. Nadaj elementom nazwy, w typie albo w literale:
Wynik:
Ana, 31
Desk lamp costs 24.99
Ana
Ana
Nazwy to udogodnienie czasu kompilacji. W czasie działania obie powyższe krotki to zwykłe wartości ValueTuple<string, int>, dlatego przypisanie person do krotki o innych nazwach się kompiluje: muszą się zgadzać tylko typy elementów i ich kolejność.
C# 7.1 i nowsze wnioskują też nazwy ze zmiennych w literale, więc var t = (name, age); daje ci t.name i t.age bez pisania ich dwa razy. W C# 7.0 te elementy to tylko Item1 i Item2.
Zwracanie wielu wartości z metody
To główny powód, dla którego krotki dodano do języka. Wcześniej do wyboru były parametry out, jednorazowa klasa albo System.Tuple z anonimowym Item1.
Wynik:
Lowest 60, highest 95
Average 78.75
Nazwy elementów w typie zwracanym stają się nazwami, które widzi wywołujący. Porównaj tę samą metodę z parametrami out:
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);
Parametry out pozostają konwencją dla kształtu TryParse, w którym bool mówi, czy się udało, a wartość wraca obok niego. Przy zadaniu "oblicz kilka rzeczy i je zwróć" krotka czyta się lepiej. Zobacz ref i out.
Dekonstrukcja
Dekonstrukcja rozpakowuje krotkę na osobne zmienne w jednej instrukcji:
Wynik:
Lima 24.5
Lima
24.5
2 1
Nazwy zmiennych w var (city, temp) wybierasz sam; nie muszą pasować do nazw elementów krotki, bo dekonstrukcja działa według pozycji. Zamiana (a, b) = (b, a) najpierw buduje krotkę ze starych wartości, a potem przypisuje, więc żadna wartość nie ginie.
Twoje własne klasy mogą obsługiwać dekonstrukcję, jeśli zadeklarujesz w nich metodę Deconstruct z parametrami out. Od C# 7 z .NET Core 2.0 lub nowszym ma ją też KeyValuePair, więc pętla po słowniku może rozpakować każdy wpis w nagłówku:
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}");
}
Ta sama forma foreach (var (a, b) in listOfTuples) działa dla listy krotek. Równie dobrze działa pobranie całej krotki jako jednej zmiennej pętli i odczytywanie jej nazwanych elementów, jak w następnej sekcji.
Krotki w listach, LINQ i słownikach
Lista nazwanych krotek to lekki sposób na przechowywanie wierszy danych wewnątrz metody:
Wynik:
Chloe 95
Ana 88
Ben 72
(Ana, B), (Chloe, A)
Ben
False
ValueTuple implementuje Equals i GetHashCode element po elemencie, więc (2, 7) zbudowane w dwóch różnych miejscach znajduje ten sam wpis w słowniku. Dzięki temu krotka to najprostszy klucz złożony: bez klasy, bez ręcznie pisanej funkcji skrótu, bez łączenia stringów w stylu row + ":" + col.
Równość krotek
Equals porównuje element po elemencie i działa od momentu wprowadzenia krotek:
Wynik:
True
False
C# 7.3 dodał dla krotek == i !=, które kompilują się do tego samego porównania element po elemencie i też ignorują nazwy:
if ((order.Status, order.Paid) == ("shipped", true)) { ... }
Console.WriteLine((1, "x") == (1, "x")); // True
System.Tuple a ValueTuple
.NET 4 wprowadził System.Tuple, klasę tworzoną przez Tuple.Create. Nadal jest we frameworku i nadal pojawia się w starszym kodzie i API, dlatego wyszukiwania "C# tuple" często na nią trafiają.
ValueTuple (C# 7) | System.Tuple (.NET 4) | |
|---|---|---|
| Składnia | (1, "a"), (int, string) | Tuple.Create(1, "a"), Tuple<int, string> |
| Rodzaj | struktura (typ wartościowy) | klasa (typ referencyjny) |
| Nazwy elementów | Tak | Nie, tylko Item1, Item2 |
| Mutowalna | Tak, elementy to pola | Nie, właściwości tylko do odczytu |
| Dekonstrukcja | Tak | Tak, przez metody rozszerzające |
Wynik:
Ben 25
(Ben, 25)
ToValueTuple() i ToTuple() konwertują między nimi. W nowym kodzie wybieraj krotki wartościowe: niczego nie alokują na stercie i mogą mieć nazwy.
Kiedy zamiast tego użyć klasy lub rekordu
Krotki sprawdzają się najlepiej, gdy grupowanie jest lokalne i oczywiste: metoda zwraca dwie liczby, zapytanie LINQ przekazuje parę do następnego kroku, słownik potrzebuje dwuczęściowego klucza. Przejdź na nazwany typ, gdy:
- Ten sam kształt pojawia się w kilku publicznych sygnaturach metod.
(string, string, int)w pięciu miejscach to klasa, której jeszcze nie napisano. - Wartości potrzebują zachowania, walidacji albo więcej niż trzech lub czterech elementów.
- Dane przekraczają granicę, taką jak serializacja. Nazwy nie istnieją w czasie działania, więc Newtonsoft.Json zapisuje
Item1iItem2, aSystem.Text.Jsonzapisuje{}, bo elementy krotki to pola, które domyślnie pomija.
Rekordy z C# 9 dają większość wygody krotki z prawdziwą nazwą. record Score(string Name, int Points); ma wbudowaną równość wartości, czytelny ToString i dekonstrukcję. Zobacz rekordy.
Częste błędy
- Oczekiwanie, że nazwy przetrwają w czasie działania. Refleksja i serializatory widzą
Item1,Item2(albo w ogóle nic). - Zmienianie kopii. Krotki to struktury:
var t2 = t1; t2.Item1 = 5;zostawiat1bez zmian. - Krotki z wieloma elementami. Powyżej trzech lub czterech nazwany typ czyta się lepiej.
- Pomylenie kolejności dekonstrukcji. Działa według pozycji: przy krotce
(Name, Age)var (age, name) = person;umieszcza imię wage, a ponieważvarwnioskuje typy, nic cię nie ostrzeże.
Najczęściej zadawane pytania
Jak zwrócić wiele wartości z metody w C#?
Zwróć krotkę: zadeklaruj typ zwracany jako (int Min, int Max) i napisz return (lowest, highest);. Wywołujący odczytuje result.Min i result.Max albo dekonstruuje przez var (min, max) = MinMax(data);. Alternatywą są parametry out, które pasują do wzorca TryParse, albo mała klasa lub rekord, gdy wynik ma własne znaczenie.
Czym jest nazwana krotka w C#?
To krotka, której elementy mają nazwy zamiast Item1, Item2: (string Name, int Age) person = ("Ana", 31); pozwala pisać person.Name. Nazwy istnieją tylko w czasie kompilacji; pod spodem to nadal ValueTuple<string, int>, a Item1 dalej działa.
Jaka jest różnica między Tuple a ValueTuple w C#?
System.Tuple (z .NET 4) to klasa: alokowana na stercie, niemutowalna, a jej elementy to zawsze tylko Item1, Item2. System.ValueTuple (C# 7) to struktura z mutowalnymi polami, obsługuje nazwy elementów i składnię (a, b) i to właśnie ją tworzy każdy literał krotki. W nowym kodzie używaj krotek wartościowych.
Jak działa dekonstrukcja krotki w C#?
var (name, age) = person; deklaruje dwie zmienne i przypisuje im po kolei elementy krotki. Możesz też przypisać do istniejących zmiennych przez (a, b) = (b, a), co je zamienia, i pomijać elementy przez discard _: var (_, age) = person;.
Czy krotki w C# można porównywać przez ==?
Od C# 7.3 == i != porównują krotki element po elemencie: (1, "a") == (1, "a") daje true. Nazwy są ignorowane; liczą się tylko pozycje i wartości. Przed C# 7.3 używaj Equals, które krotki wartościowe implementują w ten sam sposób.