1. Nazewnictwo tuple: kontekst dla dekompozycji
W poprzednim wykładzie już poznaliśmy nazwane elementy tuple, które pozwalają uczynić kod dużo czytelniejszym, odwołując się do wartości nie przez bezosobowe Item1, Item2, ale przez sensowne nazwy (na przykład person.Name). Ta możliwość nadawania nazw elementom tuple jest szczególnie ważna, gdy są one używane jako wartości zwracane z metod, parametrów lub właściwości. Właśnie nazewnictwo jest kluczowym warunkiem wygodnej dekompozycji, którą zaraz ogarniemy w tym wykładzie.
Przypominam, nazwy można ustawić przy inicjalizacji tuple przez literał, a także w sygnaturze zwracanej wartości metody, właściwości lub pola.
Przykład z metodą: Nazwane elementy tuple w zwracanej wartości metody
public static (int Wiek, string Imie) GetPetInfo()
{
return (Wiek: 5, Imie: "Fluffy");
}
// Użycie:
var info = GetPetInfo();
Console.WriteLine($"{info.Imie} — {info.Wiek} lat");
Przykład z polem/właściwością: Nazwane elementy tuple w polu/właściwości
public (int Szerokosc, int Wysokosc) ImageSize = (1024, 768);
Pamiętaj: jeśli nazwy nie są jawnie ustawione, elementy będą nadal dostępne przez Item1, Item2 i tak dalej. To może sprawić, że kod będzie mniej czytelny, zwłaszcza przy dalszym użyciu tuple. Dlatego zawsze warto nadawać sensowne nazwy elementom, jeśli ich znaczenie nie jest oczywiste z kontekstu.
2. Dekonstrukcja tuple
Co to jest dekompozycja?
Dekonstrukcja to proces "rozpakowania" tuple na osobne zmienne, żeby wygodnie z nimi pracować. Czyli z tuple (Wiek: 5, Imie: "Fluffy") można dostać dwie zmienne: wiek i imie.
Analogicznie: Wyobraź sobie, że tuple to pudełko z podpisanymi przegródkami. Dekonstrukcja to moment, gdy od razu rozkładasz zawartość pudełka na swoje miejsca na stole.
Składnia dekompozycji
var pet = (Wiek: 5, Imie: "Fluffy");
var (wiek, imie) = pet;
Console.WriteLine($"{imie} — {wiek} lat");
Teraz mamy dwie zmienne: wiek i imie. One dostały wartości z tuple. Nazwy po lewej (wiek, imie) nie muszą się pokrywać z nazwami w tuple, to po prostu nowe lokalne zmienne.
Dekonstrukcja wartości zwracanej przez funkcję
Często tuple są używane do zwracania z funkcji kilku wartości. Tam szczególnie wygodna jest dekompozycja:
public static (double min, double max) GetMinMax(int[] dane)
{
int min = dane.Min();
int max = dane.Max();
return (min, max);
}
var liczby = new[] { 1, 2, 3, 4, 5 };
var (minWartosc, maxWartosc) = GetMinMax(liczby);
Console.WriteLine($"Minimum: {minWartosc}, maksimum: {maxWartosc}");
Zwróć uwagę, że w dekompozycji zmienne możesz nazwać jak ci wygodnie w danym kontekście.
Dekonstrukcja z var
var (a, b) = (10, 20); // int a = 10, b = 20
Dekonstrukcja i discard _
Czasem nie wszystkie elementy tuple są potrzebne. Możesz je zignorować przez _ (discard). Discard (_) w tuple to po prostu sposób na powiedzenie „wiem, że tu jest jeszcze jeden element, ale nie potrzebuję go, nie twórz dla niego zmiennej”.
Czyli dekomponujesz tuple, ale zostawiasz w niej „dziurę” tam, gdzie nie potrzebujesz wartości. Zwięźle, wygodnie i bez kompromisów!
var pet = (Wiek: 5, Imie: "Fluffy", CzySzczesliwy: true);
var (wiek, _, czySzczesliwy) = pet; // tylko wiek i czySzczesliwy, imie zignorowane
Ciekawostka: discard jest często używany, żeby nie zaśmiecać przestrzeni nazw zmiennymi, których nie potrzebujesz.
Dekonstrukcja w pętli foreach
W C# możesz przeiterować tablicę tuple z dekompozycją bezpośrednio w konstrukcji foreach:
var pets = new (string Imie, int Wiek)[]
{
("Fluffy", 5),
("Kitty", 3),
("Johnny", 7)
};
foreach (var (imie, wiek) in pets)
{
Console.WriteLine($"{imie} — {wiek} lat");
}
3. Jak działają nazwy elementów i typowanie
Zachowanie nazwanych elementów
Nazwy elementów tuple to "cukier składniowy", czyli uproszczenie dla człowieka. Podczas kompilacji nazwy zamieniają się na wewnętrzne pola Item1, Item2 itd., ale IDE i kompilator zachowują nazwy, żebyś mógł ich używać.
Wpływ na kompatybilność typów i rzutowanie
Dwie tuple z taką samą liczbą elementów i typami, ale różnymi nazwami, są tym samym typem dla kompilatora. Nazwy elementów nie wchodzą w definicję typu:
var t1 = (X: 42, Y: 13);
var t2 = (A: 42, B: 13);
t1 = t2; // OK
Console.WriteLine(t1.X); // 42
Jednak przy pracy z wyrażeniami i podpowiedziami IntelliSense będą używane nazwy z lewej strony (do której przypisujesz), a nie z prawej.
Niejawne i jawne podawanie nazw
Już to przerabialiśmy, ale na wszelki wypadek przypomnę, że możesz tworzyć tuple z częściowo nazwanymi elementami, a możesz w ogóle nie podawać — wtedy będą po prostu Item1 i tak dalej.
var punkt = (X: 10, 20); // X i Item2
Console.WriteLine(punkt.X); // 10
Console.WriteLine(punkt.Item2); // 20
Porada: zawsze nazywaj elementy, jeśli tuple ma więcej niż jeden-dwa elementy, albo jeśli znaczenie wartości nie jest jasne z kontekstu.
4. Typowe błędy i szczegóły przy pracy z nazwami i dekompozycją
Błąd nr 1: „przenoszenie” nazw przy przypisaniu różnych tuple
Jeśli najpierw zadeklarowałeś tuple z jednymi nazwami, a potem przypisałeś mu inny tuple bez nazw (albo z innymi nazwami), w IntelliSense nadal będą wyświetlane pierwotne nazwy. Na przykład:
var oryginal = (X: 1, Y: 2);
var alias = oryginal; // alias.X == 1, alias.Y == 2
oryginal = (10, 20); // tuple bez nazw
Console.WriteLine(alias.X); // nadal działa, ale alias nadal trzyma stare nazwy
Błąd nr 2: duplikacja nazw elementów.
Nie możesz nadać dwóm elementom tej samej nazwy — kompilator wywali komunikat „Duplicate tuple element name”:
var zlyTuple = (A: 1, A: 2); // Błąd CS8122: Duplicate tuple element name 'A'
Błąd nr 3: zła dekompozycja po liczbie elementów.
Przy dekompozycji liczba zmiennych musi się dokładnie zgadzać z rozmiarem tuple. Każda niezgodność skończy się błędem:
var pet = (Wiek: 5, Imie: "Fluffy");
var (wiek, imie, nastroj) = pet; // Błąd CS8124: Tuple must contain exactly 3 elements
Błąd nr 4: złe użycie discard _
Ignorowanie elementów przez _ działa dla każdego miejsca osobno i nie „łączy” pominięć w jedno. Na przykład, jeśli próbujesz jednocześnie pominąć dwa elementy przez jedno _, dostaniesz błąd:
var dane = (1, 2, 3);
var (_, x, _) = dane; // OK: pominięte pierwszy i trzeci element
var (_, _) = dane; // Błąd CS8124: Tuple must contain exactly 2 elements
GO TO FULL VERSION