CodeGym /Kursy /C# SELF /Zalety kolekcji Generics

Zalety kolekcji Generics

C# SELF
Poziom 27, Lekcja 1
Dostępny

1. Krótka historia

Dawno temu w C# były tylko dwie radości przy pracy z kolekcjami: używać tablic albo tak zwanych "słabo typowanych" kolekcji — jak ArrayList, gdzie można było wrzucać obiekty dowolnego typu. Wydawałoby się, wolność! Ale wystarczyło dodać do takiej kolekcji stringa i liczbę naraz — i zaczynał się bałagan: nie da się bezpiecznie wyciągnąć elementu bez dodatkowych (i nie zawsze działających) sprawdzeń typu. A do tego — dziwne błędy znikąd i tańce z rzutowaniem typów.

Przykład nie-generic kolekcji (ArrayList):


using System.Collections;

ArrayList stuff = new ArrayList();
stuff.Add(42);
stuff.Add("Hello C#");

int number = (int)stuff[0]; // OK
string text = (string)stuff[1]; // OK

int fail = (int)stuff[1]; // BUM! InvalidCastException (runtime error)

Tak, kompilator się nie czepia — błąd wychodzi dopiero w trakcie działania! To jakby otworzyć lodówkę i znaleźć tam gorący garnek — niespodzianka!

O co chodzi z generykami?

Generyczne kolekcje (Generics) pojawiły się po to, żeby można było tworzyć kolekcje z gwarantowanym typem zawartości. To daje trzy kluczowe zalety:

  • Bezpieczeństwo typów już na etapie kompilacji. Kompilator nie pozwoli Ci przypadkiem wrzucić do kolekcji czegoś niepasującego.
  • Wygoda: nie musisz za każdym razem ręcznie rzutować typu.
  • Wydajność: nie tracisz czasu na niepotrzebne "pakowanie i rozpakowywanie" typów wartościowych (boxing/unboxing).

2. Czym w ogóle są Generics (generyki)?

Generics — to magiczny sposób opisywania struktur danych i metod tak, żeby działały z dowolnym typem, ale nadal były ściśle typowane.

Wyobraź sobie uniwersalne pudełko, którego możesz użyć zarówno na książki, jak i na skarpetki, ale tylko dla jednego typu naraz. Jeśli pudełko jest zadeklarowane jako "tylko na książki" — nikt nie włoży tam skarpetek. Tak samo z generic-kolekcjami: jeśli tworzysz kolekcję z int, nie wpadnie tam przypadkiem string.

Przykład deklaracji generycznej klasy


public class Box<T>
{
    public T Value { get; set; }
}

var boxOfInt = new Box<int> { Value = 42 };
var boxOfString = new Box<string> { Value = "Hello Generics!" };

T — to "parametr typu", który określa, z czym będzie pracować Twoje pudełko. C# wymaga, żebyś zawsze jasno określił, jaki typ wkładasz.

Generyczne kolekcje w .NET

W .NET Framework praktycznie wszystkie nowoczesne kolekcje mają wersje generic. To:

  • List<T> — dynamiczna lista elementów typu T.
  • Dictionary<TKey, TValue> — tablica asocjacyjna (słownik) z kluczami i wartościami.
  • Queue<T>, Stack<T> — kolejki i stosy.
  • i wiele innych.

3. Jak to działa: mechanizm Generics

Parametry typu

Kiedy deklarujesz kolekcję jak List<int>, kompilator C# tworzy osobną wersję (specialization) tej klasy specjalnie dla typu int. Gdy deklarujesz List<string>, kompilator C# robi kolejną wersję tej klasy, ale już dla stringów itd.

A dla Ciebie, jako programisty, to działa jak magia:


List<int> numbers = new List<int>();
numbers.Add(1); // Można dodać tylko int

List<string> words = new List<string>();
words.Add("hello"); // Można dodać tylko string

Przy próbie dodania elementu innego typu kompilator od razu się obrazi:


numbers.Add("fail"); // BŁĄD na etapie kompilacji!

Bezpieczeństwo typów: Compile-time vs. Run-time

To znaczy, że błędy jak powyżej nie dotrą do fazy uruchamiania programu (run-time). Twoja kolekcja jest tak zabezpieczona, że nie da się jej oszukać — kompilator stoi na straży jak cerber przy drzwiach.

Pod maską

  • Dla typów referencyjnych (np. string, object) nie ma już niepotrzebnych rzutowań typów.
  • Dla typów wartościowych (int, double) znika problem "pakowania/rozpakowywania" (boxing/unboxing).
  • Użycie generics w .NET nie prowadzi do nadmiaru kodu — CLR optymalizuje to na etapie JIT-kompilacji.

4. Jak generics zwiększają wydajność

Podkreślmy jeszcze raz te magiczne właściwości, które Generics dają naszemu kodowi:

Bezpieczeństwo typów (Type Safety):

To chyba najważniejsze. Dzięki Generics kompilator staje się Twoim osobistym ochroniarzem, który nie wpuści "obcych" do Twojej kolekcji. Możesz być absolutnie pewny, że List<Product> będzie zawierać tylko obiekty Product, a nie jakieś przypadkowe stringi czy liczby. To eliminuje całą klasę błędów, które wcześniej mogły wyjść dopiero podczas działania programu i prowadzić do niespodziewanych "wybuchów" (InvalidCastException). Twój kod staje się bardziej niezawodny i przewidywalny.

Wydajność (Performance):

Jak już mówiliśmy, dla typów wartościowych (jak int, double czy DateTime) Generics pozwalają efektywniej zarządzać pamięcią i czasem procesora. Dla kolekcji, które przechowują miliony elementów albo są często modyfikowane, to może być kluczowy czynnik. Zamiast ciągle przekładać pomarańcze z pudełka do worka i z powrotem, po prostu wkładasz je od razu do właściwego pudełka.

Wielokrotne użycie kodu (Code Reusability):

Generics pozwalają pisać ten sam kod, który działa z różnymi typami danych bez konieczności jego powielania. Załóżmy, że potrzebujesz funkcji do zamiany miejscami dwóch zmiennych. Bez Generics musiałbyś pisać SwapInt(ref int a, ref int b), SwapString(ref string a, ref string b), SwapProduct(ref Product a, ref Product b) i tak dalej. Z Generics piszesz tylko jedną funkcję: Swap<T>(ref T a, ref T b). Ta zasada dotyczy też kolekcji: nie potrzebujesz IntList, StringList, ProductList – wystarczy List<T>. To sprawia, że Twój kod jest bardziej zwięzły, łatwy w utrzymaniu i skalowalny.

5. Generyczne metody i własne klasy generic

Generyczne metody

Generics — to nie tylko kolekcje! Możesz pisać własne generyczne metody, które działają z dowolnym typem. To sprawia, że kod jest bardziej uniwersalny.


//Podajemy parametr typu T zaraz po nazwie metody
public static void Swap<T>(ref T x, ref T y)
{
    T temp = x;
    x = y;
    y = temp;
}

// Użycie:	
int a = 10, b = 20;
Swap(ref a, ref b); // Teraz a == 20, b == 10

string one = "jeden", two = "dwa";
Swap(ref one, ref two); // Działa też dla stringów!

Zwróć uwagę, że kompilator C# może sam określić parametr typu metody na podstawie przekazanych zmiennych. Dlatego w powyższym przykładzie do metody Swap nie trzeba dodatkowo podawać typu.

Własne klasy generic

Możesz nawet tworzyć własne "pudełka" (klasy generic). To prostsze niż myślisz:


public class Pair<TFirst, TSecond>
{
    public TFirst First { get; set; }
    public TSecond Second { get; set; }
}

// Użycie:
var pair = new Pair<int, string> { First = 42, Second = "odpowiedź" };

6. Przykład użycia generic-kolekcji

Wróćmy do naszej "listy zadań" (To-Do List), którą zaczęliśmy budować w przykładach z poprzednich wykładów. Wcześniej przechowywaliśmy zadania w tablicy albo po prostu wypisywaliśmy je na ekran. Teraz — trzymamy je dynamicznie w List<string>.

Przykład: dynamiczne dodawanie zadań do listy


using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        List<string> tasks = new List<string>();

        Console.WriteLine("Wpisz zadanie (albo pusty wiersz, żeby zakończyć):");
        string input;
        while (!string.IsNullOrWhiteSpace(input = Console.ReadLine()))
        {
            tasks.Add(input);
            Console.WriteLine("Zadanie dodane! Wpisz kolejne (albo pusty wiersz, żeby zakończyć):");
        }

        Console.WriteLine("\nTwoja lista zadań na dziś:");
        foreach (string task in tasks)
        {
            Console.WriteLine("- " + task);
        }
    }
}

Co tu jest fajnego?

  • Kolekcja automatycznie się powiększa, gdy dodajesz nowe zadania.
  • Nie da się przypadkiem dodać do listy czegoś innego niż string.
  • Możesz łatwo przejść przez listę i wypisać jej zawartość.

7. Typowe błędy początkujących, cechy i wskazówki

Przesiadka z nie-generic kolekcji na generic czasem powoduje zamieszanie. Oto kilka typowych sytuacji, z którymi spotykają się początkujący:

  • Próba dodania elementu złego typu (List<int> numbers = new List<int>(); numbers.Add("hi"); // Błąd).
  • Chęć mieszania typów w jednej kolekcji — wtedy trzeba wybrać typ bazowy (np. List<object>) — i potem zawsze rzutować z powrotem na właściwy typ.
  • Zapominają o istnieniu constraints, piszą zbyt "szerokie" generyki i łapią dziwne błędy na etapie kompilacji.
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION