CodeGym /Kursy /C# SELF /Interfejs IComparer<T&...

Interfejs IComparer<T>

C# SELF
Poziom 30 , Lekcja 1
Dostępny

1. Wprowadzenie

Wyobraź sobie, że jesteś dziekanem uniwersytetu, gdzie uczy się masa studentów. Regularnie potrzebujesz list posortowanych według różnych kryteriów:

  1. Według imienia (żeby szybko znaleźć studenta alfabetycznie).
  2. Według średniej ocen (żeby przyznać stypendia najlepszym).
  3. Według wieku (do statystyk, konkursów itd.).
  4. Według roku studiów, a w ramach roku — według nazwiska.

Gdybyśmy polegali tylko na IComparable<T>, nasza klasa Student musiałaby mieć tylko jeden sposób porównywania. Na przykład uznalibyśmy, że "naturalny" porządek studenta to według średniej ocen. Super, List.Sort() działa! Ale co, jeśli potrzebujemy listy posortowanej według imienia? Klasa Student już jest "zajęta" porównywaniem po ocenach. Nie może mieć dwóch "naturalnych" porządków naraz. To tak, jakbyś miał tylko jedną instrukcję "jak być najlepszym bokserem", a nagle musisz być mistrzem szachów i próbujesz użyć "bokserskiej" instrukcji. Raczej nie wyjdzie, co nie?

Właśnie do takich sytuacji, gdy trzeba sortować ten sam typ obiektów na różne sposoby i nie chcesz wrzucać logiki porównywania do samej klasy, jest interfejs IComparer<T>. Przydaje się, jeśli nie masz dostępu do kodu klasy albo klasa nie powinna wiedzieć o wszystkich sposobach sortowania.

2. Interfejs IComparer<T>

Jeśli IComparable<T> to takie wewnętrzne przeczucie obiektu, wiedza jak porównać siebie z innym, to IComparer<T> to zupełnie osobny, zewnętrzny sędzia albo niezależny arbiter, który bierze dowolnych dwóch graczy (obiekty) i porównuje ich według własnych, ustalonych zasad.

Wyobraź sobie, że jesteś trenerem drużyny piłkarskiej. Musisz wybrać kapitana.

  • IComparable<T>: Każdy gracz sam mówi: "Jestem lepszy od tego gościa, bo szybciej biegam!" (jego własna, wewnętrzna zasada).
  • IComparer<T>: Ty, jako trener, mówisz: "Dobra, dziś wybieramy kapitana według celności podań. Piotrek, podaj! Wojtek, podaj! Super, Piotrek celniej! On dziś jest kapitanem." (To twoja, zewnętrzna zasada, którą stosujesz do dowolnych dwóch graczy).

Definicja: IComparer<T> to interfejs w .NET, który pozwala nam określić zewnętrzną logikę porównywania dla dwóch obiektów typu T. To znaczy, że klasa implementująca IComparer<T> nie jest jednym z porównywanych obiektów; ona po prostu udostępnia metodę Compare, która przyjmuje dwa obiekty i określa ich wzajemny porządek.

Składnia

Składnia interfejsu IComparer<T> jest całkiem prosta:


public interface IComparer<T>
{
    // Metoda, która porównuje dwa obiekty typu T
    // x - pierwszy obiekt do porównania
    // y - drugi obiekt do porównania
    int Compare(T x, T y);
}

Metoda Compare(T x, T y) działa dokładnie tak samo jak metoda CompareTo(T other) w IComparable<T>:

  • Zwraca liczbę ujemną, jeśli x jest "mniejszy" od y.
  • Zwraca zero (0), jeśli x jest "równy" y.
  • Zwraca liczbę dodatnią, jeśli x jest "większy" od y.

W naszym przypadku "mniejszy", "równy", "większy" zależy tylko od naszej logiki porównywania, którą ustalimy w implementacji Compare.

Czym się różni IComparer<T> od IComparable<T>

Klasa / Interfejs Gdzie się implementuje Do czego służy Przykład użycia
IComparable<T>
Bezpośrednio w typie (klasie/strukturze) Jeden standardowy sposób porównywania Sortowanie rosnąco po ID
IComparer<T>
W osobnej klasie Dowolna liczba sposobów porównywania Sortowanie po imieniu, dacie

3. Implementacja IComparer<T> w praktyce

Rozwijajmy dalej naszą "jedną wielką aplikację" — prosty model użytkowników. Załóżmy, że mamy taką klasę:


// Nasza klasa użytkownika
public class User
{
    public string Name { get; set; }
    public int Age { get; set; }
    public string Email { get; set; }
}

Sortowanie po imieniu: tworzymy osobny komparator

Napiszmy specjalną klasę implementującą IComparer<User>, która będzie porównywać użytkowników po imieniu:


// Klasa-komparator do sortowania po imieniu
public class UserNameComparer : IComparer<User>
{
    public int Compare(User x, User y)
    {
        // Sprawdzamy na null (żeby nie było niespodzianek!)
        if (ReferenceEquals(x, y)) return 0;
        if (x is null) return -1;   // null "mniejszy" od każdego obiektu
        if (y is null) return 1;

        // Porównujemy po imieniu (z uwzględnieniem standardu sortowania stringów)
        return string.Compare(x.Name, y.Name, StringComparison.OrdinalIgnoreCase);
    }
}

Używamy komparatora do sortowania listy:


List<User> users = new List<User>
{
    new User { Name = "Jan", Age = 20, Email = "jan@mail.com" },
    new User { Name = "Anna", Age = 32, Email = "anna@gmail.com" },
    new User { Name = "Borys", Age = 28, Email = "borys@work.org" },
    new User { Name = "Ruslan", Age = 19, Email = "ruslan@yandex.ru" }
};

// Sortujemy po imieniu za pomocą IComparer
users.Sort(new UserNameComparer());

users.ForEach(u => Console.WriteLine(u.Name)); // Anna, Borys, Jan, Ruslan

Widzisz, jakie to ładne i eleganckie? Klasa-komparator staje się niezależnym obywatelem twojego programu — możesz jej użyć do dowolnych innych list użytkowników.

4. Wiele wariantów porównywania: tworzymy różne komparatory

Ciekawe jest to, że możesz stworzyć tyle komparatorów, ile chcesz. Na przykład zróbmy sortowanie po wieku:


// Komparator do sortowania po wieku
public class UserAgeComparer : IComparer<User>
{
    public int Compare(User x, User y)
    {
        if (ReferenceEquals(x, y)) return 0;
        if (x is null) return -1;
        if (y is null) return 1;

        // Sortujemy rosnąco po wieku
        return x.Age.CompareTo(y.Age);
    }
}

I teraz:


users.Sort(new UserAgeComparer());
users.ForEach(u => Console.WriteLine($"{u.Name} ({u.Age})"));
// Wynik: Ruslan (19) Jan (20) Borys (28) Anna (32)

Jeśli chcesz posortować po wieku malejąco, wystarczy zamienić argumenty miejscami:


// Komparator do sortowania malejąco po wieku
public class UserAgeDescendingComparer : IComparer<User>
{
    public int Compare(User x, User y)
    {
        if (ReferenceEquals(x, y)) return 0;
        if (x is null) return -1;
        if (y is null) return 1;

        // Zmieniamy kolejność: y.CompareTo(x)
        return y.Age.CompareTo(x.Age);
    }
}

5. Przydatne niuanse

Jak to działa w środku?

Kiedy wywołujesz Sort() z komparatorem, lista przekazuje każdy element temu komparatorowi i pyta: "Kogo ustawić wcześniej?". Twój Compare odpowiada: "tego, tego albo nie trzeba zmieniać". Sortowanie powtarza ten dialog dla wszystkich par, aż powstanie końcowa, posortowana lista.

Co zrobić, jeśli wartości są równe? Po prostu zwróć 0 — to znaczy, że kolejność elementów się nie zmieni (albo zdecydują o tym wewnętrzne mechanizmy sortowania).

Gdzie jeszcze używa się IComparer<T>?

Interfejs IComparer<T> jest używany nie tylko w listach. Oto kilka przykładów, gdzie pojawia się w .NET:

Przykład:


var sortedSet = new SortedSet<User>(new UserAgeComparer());

Teraz SortedSet zawsze będzie automatycznie utrzymywać porządek po wieku!

Krótko o bezpieczeństwie null

Jednym z najczęstszych błędów, z którymi spotykają się początkujący, jest NullReferenceException. Nie zapominaj sprawdzać obiektów na null w środku Compare, szczególnie jeśli lista może zawierać takie wartości.

Częsty wzorzec (podajemy jeszcze raz, dla pewności):


if (ReferenceEquals(x, y)) return 0;
if (x is null) return -1;
if (y is null) return 1;

To dobra praktyka: pomaga uniknąć wywalenia programu w najmniej odpowiednim momencie!

Zalety i ograniczenia podejścia z IComparer<T>

  • Wyraźnie oddziela logikę porównywania od danych. Klasa użytkownika nie musi się zastanawiać, jak i dlaczego będzie sortowana.
  • Łatwo ponownie użyć logiki porównywania w różnych miejscach.
  • Zapewnia skalowalność: możesz tworzyć dowolną liczbę wariantów sortowania bez zmiany oryginalnego typu.

Ale uważaj, żeby nie wpaść w sytuację "zapomniałem o null" albo "logika porównywania się nie zgadza". Na przykład, jeśli Compare(x, y) zwraca 0, to Compare(y, x) też musi zwrócić 0; jeśli Compare(x, y) zwraca >0, to Compare(y, x) musi zwrócić <0 itd.

Wizualna ściąga: kiedy czego używać?

Zadanie Czego użyć Gdzie zaimplementować logikę
Jedno "naturalne" sortowanie
IComparable<T>
W samym typie (klasie/strukturze)
Różne warianty sortowania
IComparer<T>
W osobnej klasie
Szybko, jednorazowo, "na biegu"
Comparison<T>
/ lambda
W parametrze metody Sort, przez delegat
Złożona, często używana logika
IComparer<T>
Jako osobny klasa-komparator

W następnym wykładzie poznasz, jak używać i łączyć delegaty oraz wyrażenia lambda do porównywania obiektów. A póki co spróbuj w swojej aplikacji zaimplementować kilka różnych komparatorów i ciesz się elegancką architekturą, gdzie sortowanie jest poza główną klasą, a wybór kryterium — elastyczny i łatwy do rozbudowy.

6. Typowe błędy przy implementacji komparatorów

Błąd nr 1: brak sprawdzenia na null.
Jeśli jeden z obiektów w porównaniu jest null, a nie przewidziałeś tego w kodzie, program może się wywalić z NullReferenceException.

Błąd nr 2: nieprawidłowe wartości -1, 0, +1.
Metoda Compare powinna zwracać liczbę ujemną, jeśli pierwszy obiekt jest mniejszy od drugiego, zero — jeśli są równe, i liczbę dodatnią — jeśli większy. Złamanie tej zasady prowadzi do "dziwnego" działania sortowania.

Błąd nr 3: asymetryczna logika porównywania.
Jeśli przy porównaniu x i y zwracasz coś, a przy porównaniu y i x — to samo (zamiast przeciwnej wartości), wynik staje się nieprzewidywalny.

Błąd nr 4: użycie Sort() bez komparatora dla własnego typu.
Jeśli typ nie implementuje IComparable ani IComparable<T>, to wywołanie Sort() bez jawnego komparatora skończy się wyjątkiem InvalidOperationException.

Jak uniknąć:
Sprawdzaj przypadki brzegowe, pokrywaj krytyczne fragmenty kodu testami jednostkowymi (do tego jeszcze wrócimy!), nie bój się zaglądać do dokumentacji — i niech twoje komparatory działają jak szwajcarski zegarek.

2
Zadanie
C# SELF, poziom 30, lekcja 1
Niedostępne
Implementacja komparatora dla stringów
Implementacja komparatora dla stringów
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION