Menu

Krotki w C# (tuple): nazwane krotki, zwracanie wielu wartości, dekonstrukcja

Krotka w C# łączy kilka wartości w jedną bez deklarowania typu. Poznaj składnię krotek wartościowych, nazwane elementy, zwracanie wielu wartości z metody, dekonstrukcję przez var (a, b), krotki jako klucze słownika oraz czym ValueTuple różni się od starszego System.Tuple.

Na tej stronie są działające edytory: edytuj, uruchamiaj i od razu zobacz wynik.

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>
Rodzajstruktura (typ wartościowy)klasa (typ referencyjny)
Nazwy elementówTakNie, tylko Item1, Item2
MutowalnaTak, elementy to polaNie, właściwości tylko do odczytu
DekonstrukcjaTakTak, 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 Item1 i Item2, a System.Text.Json zapisuje {}, 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; zostawia t1 bez 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ę w age, a ponieważ var wnioskuje 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.

Ilustracja języków programowania w Coddy

Ucz się programowania z Coddy

ZACZNIJ