Zasadniczo Tuple (krotka) to uporządkowany ciąg, niezmienny, stałej wielkości i różnorodnych obiektów, np. każdy obiekt określonego typu.

Krotki nie są nowe w programowaniu. Są już wykorzystywane m.in. w Python oraz w bazach danych. Jednakże, jest to nowość w języku C#. Krotka została wprowadzona w C# 4.0.

Do naszej dyspozycji zostało oddane 8 metod statycznych pozwalających na tworzenie krotek:

  • Create(T1)
  • Create(T1, T2)
  • Create(T1, T2, T3)
  • Create(T1, T2, T3, T4)
  • Create(T1, T2, T3, T4, T5)
  • Create(T1, T2, T3, T4, T5, T6)
  • Create(T1, T2, T3, T4, T5, T6, T7)
  • Create(T1, T2, T3, T4, T5, T6, T7, T8)
Do tego celu możemy również użyć jednego z konstruktorów klasy Tuple:
  • Tuple<T1>
  • Tuple<T1, T2>
  • Tuple<T1, T2, T3>
  • Tuple<T1, T2, T3, T4>
  • Tuple<T1, T2, T3, T4, T5>
  • Tuple<T1, T2, T3, T4, T5, T6>
  • Tuple<T1, T2, T3, T4, T5, T6, T7>
  • Tuple<T1, T2, T3, T4, T5, T6, T7, T8>
Tuple ma ograniczenie do 8 elementów. Jeżeli chcemy utworzyć krotkę z większą niż 8 liczbą elementów należy przygotować zagnieżdżone krotki.
Ośmy element naszej krotki musi koniecznie być inną krotką. Przykład pokazany poniżej zwraca wyjątek:
// W trakcie kompilacji wyskoczy błąd: The last element of an eight element tuple must be a Tuple.
var tupleExmaple = new Tuple<int, int, int, int, int, int, int, int>(1, 2, 3, 4, 5, 6, 7, 8);
Aby poprawnie przygotować przykład dla 8-miu elementów należy:
// poprawna deklaracja
var tupleCorrectExample = new Tuple<int, int, int, int, int, int, int, Tuple<int>>(1, 2, 3, 4, 5, 6, 7, Tuple.Create(8));
Z kolei aby zdefiniować krotkę z większą niż 8 liczbą elementów należy użyć poniższej konstrukcji:
// poprawna deklaracja dla większej niż 8 liczby elementów
var tupleMoreThan8 = new Tuple<int, int, int, int, int, int, int, Tuple<int, int, int>>(1, 2, 3, 4, 5, 6, 7, new Tuple<int, int, int>(9, 10, 11));


Co reprezentuje krotka?

Krotka nie posiada nazwy, która mogłby mieć znaczenie (jak w przypadku metod). Atrybuty krotki nazywane są jako kolejne elementy, tj. Item1, Item2, itd.

Dwie krotki mogą być równe, ale nie oznacza to, że są takie same. Znaczenie takie nie jest jednoznacze co może wpłynąć na czytelność kodu.

Dla przykładu, dwie poniższe krotki są równe, ale reprezetnują różne rzeczy:
(3,9): Kod produktu: 3 oraz ilość: 9;
(3,9): 3 oraz 9 to idenfikatory klientów zwracane przez zapytanie do bazy danych.

Jak widzicie na powyższym przykładzie, krotki nie przechwoują informacji o znaczeniu, ich użycie ma charakter ogólny (generyczny) i to programista decyduje jakie jest znaczenie krotki w momencie, gdy ją tworzy.


Dlaczego warto z nich korzystać?

Krotka pozwala na szybkie grupowanie wielu wartości w jeden rezultat, który może być szczególnie przydatny, gdy chcemy zwrócić te wartości ale nie chcemy używać do tego parametrów metody takich jak out lub/i ref.
Zwracanie danych z metody

using System;
namespace TupleReturnMethod
{
    class Program
    {
        static void Main(string[] args)
        {
            var carData = CarInformation();
            // Zgodnie z definicją, kolejne elementy to Item1, Item2, etc.
            Console.WriteLine("Numer auta: {0}", carData.Item1);
            Console.WriteLine("Auto: {0}", carData.Item2);
            Console.WriteLine("Rocznik: {0}", carData.Item3.ToShortDateString());
            Console.ReadKey();
            // Wynik działania programu
            // Numer auta: 1
            // Auto: Audi RS6
            // Rocznik: 2010-08-08
        }
        static Tuple CarInformation()
        {
            // Pierwsza z możliwości utworzenia krotki
            Tuple<int, string, DateTime> carData = new Tuple<int, string, DateTime>(1, "Audi RS6", new DateTime(2010, 8, 8));
            // Drugi sposób tworzenia krotki
            Tuple<int, string, DateTime> carData2 = Tuple.Create(1, "Audi RS6", new DateTime(2010, 8, 8));
            return carData;
        }
    }
}
Kolejnym przykładem może być utworzenie słownika o złożonym kluczu dostępu do konkretnego elementu.
Złożony klucz w słowniku
using System;
using System.Collections.Generic;
namespace KeyInDictionary
{
    class Program
    {
        static void Main(string[] args)
        {
            var list = CarsList();
            // Tworzymy nasz złożony klucz przy użyciu klasy Tuple
            var access = Tuple.Create(1, "Audi");
            // Uzyskujemy dostęp do modelu samochodu po podaniu złożonego klucza
            Console.WriteLine("Dostęp do elementu: {0}", list[access].Model);
            Console.ReadKey();
            // Wynik działania programu
            // Dostep do elementu: RS6
        }
        public static Dictionary<Tuple<int, string>, Car> CarsList()
        {
            // Tworzymy słownik o złożonym kluczu, składa się z identyfikatora i marki samochodu
            Dictionary<Tuple<int, string>, Car> list = new Dictionary<Tuple<int, string>, Car>();
            Car c1 = new Car();
            c1.Id = 1;
            c1.Brand = "Audi";
            c1.Model = "RS6";
            Car c2 = new Car();
            c2.Id = 2;
            c2.Brand = "BMW";
            c2.Model = "M3";
            // Dodajemy utworzone wyżej obiekty
            // Klucz słownika składa się z Id oraz marki samochodu
            list.Add(Tuple.Create(c1.Id, c1.Brand), c1);
            list.Add(Tuple.Create(c2.Id, c2.Brand), c2);
            return list;
        }
    }
    public class Car
    {
        public int Id { get; set; }
        public string Brand { get; set; }
        public string Model { get; set; }
    }
}
Zastępowanie klas lub struktur stworzonych jedynie do przechowywania danych lub wypełnienia listy

Używając Tuple nie ma potrzeby tworzenia klas czy struktur, które służą jedynie do przechowywania danych tymczasowych. Poniższy przykład pokazuje taką koncepcję:

using System;
using System.Collections.Generic;
using System.Linq;
namespace TupleInsteadOfClassOfStruct
{
    class Program
    {
        static List<Tuple<int, string>> carList = new List<Tuple<int, string>>();
        static void Main(string[] args)
        {
            // Dodajemy wartości do naszej listy
            carList.Add(Tuple.Create(71, "Audi"));
            carList.Add(Tuple.Create(62, "Pagani"));
            carList.Add(Tuple.Create(53, "Lamborghini"));
            carList.Add(Tuple.Create(54, "Ferrari"));
            carList.Add(Tuple.Create(45, "Nissan"));
            carList.Add(Tuple.Create(36, "Toyota"));
            carList.Add(Tuple.Create(27, "Subaru"));
            carList.Add(Tuple.Create(18, "BMW"));
            // Sortujemy po nazwie
            carList = ReturnListSortedByName();
            Console.WriteLine("Elementy posortowane po nazwie");
            foreach (var item in carList)
            {
                // Identyfikatory nie są potrzebne, wyświetlimy tylko nazwy
                Console.WriteLine(item.Item2);
            }
            Console.WriteLine("=======================================");
            carList = ReturnListSorderById();
            Console.WriteLine("Elementy posortowane po Id");
            foreach (var item in carList)
            {
                Console.WriteLine("Id: {0}, Marka: {1}", item.Item1, item.Item2);
            }
            Console.ReadKey();
            // Wynik działania programu
            // Elementy posortowane po nazwie
            // Audi
            // BMW
            // Ferrari
            // Lamborghini
            // Nissan
            // Pagani
            // Subaru
            // Toyota
            // =======================================
            // Elementy posortowane po Id
            // Id: 18, Marka: BMW
            // Id: 27, Marka: Subaru
            // Id: 36, Marka: Toyota
            // Id: 45, Marka: Nissan
            // Id: 53, Marka: Lamborghini
            // Id: 54, Marka: Ferrari
            // Id: 62, Marka: Pagani
            // Id: 71, Marka: Audi
        }
        static List<Tuple<int, string>> ReturnListSortedByName()
        {
            // sortowanie elementów po nazwie
            var sorted = carList.OrderBy(t => t.Item2).ToList();
            return sorted;
        }
        static List<Tuple<int, string>> ReturnListSorderById()
        {
            // sortowanie elementów po id
            var sorted = carList.OrderBy(t => t.Item1).ToList();
            return sorted;
        }
    }
}
Porównywanie i sortowanie
Krótka jest równa innej krotce wtedy i tylko wtedy, gdy wszystkie elementy są takie same, tj. t1.Item1 == t2.Item2 oraz t1.Item2 == t2.Item2.

Sortowanie: porównanie jest dokonywane na poszczególnych pozycjach, tj. w pierwszej kolejności sprawdzamy, jeśli t1.Item1 > t2.Item1, to krotka 2 jest mniejsza, jeśli t1.Item1 == t2.Item1 to wówczas porównywanie odbywa się z Item2 i tak dalej.
Aby używać interfejsów IComparable, IEquatable, IStructuralComparable oraz IStructuralEquatable należy dokonać jawnego rzutowania do żądanego interfejsu.
using System;
namespace TuplesComparingOrder
{
    class Program
    {
        static void Main(string[] args)
        {
            Tuple<int, int> t1 = Tuple.Create(3, 9);
            Tuple<int, int> t2 = Tuple.Create(3, 9);
            Tuple<int, int> t3 = Tuple.Create(9, 3);
            Tuple<int, int> t4 = Tuple.Create(9, 4);
            // Equals sprawdza zawartość
            Console.WriteLine("t1 = t2: {0}", t1.Equals(t2)); // true
            // == sprawdza referencje, jest inna
            Console.WriteLine("t1 = t2: {0}", t1 == t2); // false 
            Console.WriteLine("t1 = t3: {0}", t1.Equals(t3)); // false
            Console.WriteLine("t1 < t3 : {0}", ((IComparable)t1).CompareTo(t3) < 0); // true
            Console.WriteLine("t3 < t4 : {0}", ((IComparable)t3).CompareTo(t4) < 0); // true
            Console.ReadKey();
        }
    }
}
Jeżeli powyższe komenatrze dotyczące wartości logicznych nie są dla Ciebie jasne odsyłam do artykułu -> "C# - różnica pomiędzy '==' a Equals()".


Wnioski

Częste używanie klasy Tuple wpływa na czytelność kodu. Jednakże jego wykorzystanie może być bardzo przydatne dla programistów pozwalając im na zwracanie wielu wartości bez konieczności tworzenia parametrów takich jak ref i/lub out. Użycie krotek pozwala również na tworzenie złożonych kluczy w kolekcjach takich jak Dictinary (słownik) oraz elimuje konieczność tworzenia klas i struktur tylko po to by przechowywać w nich dane tymczasowe.