JavaRush /Курси /C# SELF /Інтерфейс IComparer<T&...

Інтерфейс IComparer<T>

C# SELF
Рівень 30 , Лекція 1
Відкрита

1. Вступ

Уявіть, що ви — декан в університеті, де навчається багато студентів. Вам постійно потрібні списки, відсортовані за різними критеріями:

  1. За імʼям (щоб швидко знайти студента за абеткою).
  2. За середнім балом (щоб видати гранти відмінникам).
  3. За віком (для статистики, конкурсів тощо).
  4. За курсом, а всередині курсу — за прізвищем.

Якби ми покладалися тільки на IComparable<T>, нашому класу Студент довелося б підтримувати лише один спосіб порівняння. Наприклад, ми б вирішили, що «природний» порядок студента — це за середнім балом. Чудово, List.Sort() тепер працює! Але що, якщо нам потрібен список, відсортований за імʼям? Клас Студент уже «зайнятий» порівнянням за балом. Він не може мати два «природні» порядки одночасно. Це якби у вас була лише одна інструкція «як бути найкращим у боксі», а вам терміново потрібно стати кращими в шахах і ви спробували б застосувати «боксерську» інструкцію. Навряд чи це дало б результат, чи не так?

Саме для таких сценаріїв, коли потрібно сортувати один і той самий тип об’єктів різними способами й не варто вбудовувати логіку порівняння безпосередньо в клас, існує інтерфейс IComparer<T>. Він корисний, якщо у вас немає доступу до коду класу або якщо клас не повинен знати про всі способи сортування.

2. Інтерфейс IComparer<T>

Якщо IComparable<T> — це внутрішнє «чуття» об’єкта, знання про те, як порівняти себе з іншим, то IComparer<T> — це окремий зовнішній суддя або незалежний арбітр, який бере будь-які два об’єкти й порівнює їх за своїми, заздалегідь визначеними правилами.

Уявіть, що ви — тренер футбольної команди. Вам потрібно обрати капітана.

  • IComparable<T>: Кожен гравець сам каже: «Я кращий за цього гравця, бо швидше бігаю!» (його власне, внутрішнє правило).
  • IComparer<T>: Ви, як тренер, визначаєте правило: «Сьогодні обираємо капітана за точністю пасу. Петре, пасуйте! Василю, пасуйте! Чудово, у Петра точніше. Він сьогодні капітан» (це ваше, зовнішнє правило, яке застосовується до будь-яких двох гравців).

Визначення: IComparer<T> — це інтерфейс у .NET, який дозволяє задати зовнішню логіку порівняння для двох об’єктів типу T. Це означає, що клас, який реалізує IComparer<T>, не є одним із порівнюваних об’єктів; він лише надає метод Compare, що приймає два об’єкти і визначає їхній відносний порядок.

Синтаксис

Синтаксис інтерфейсу IComparer<T> дуже простий:


public interface IComparer<T>
{
    // Метод, який порівнює два об'єкти типу T
    // x - перший об'єкт для порівняння
    // y - другий об'єкт для порівняння
    int Compare(T x, T y);
}

Метод Compare(T x, T y) працює так само, як і метод CompareTo(T other) у IComparable<T>:

  • Повертає від’ємне число, якщо x «менше» y.
  • Повертає нуль (0), якщо x «дорівнює» y.
  • Повертає додатне число, якщо x «більше» y.

У нашому випадку «менше», «дорівнює», «більше» залежать лише від логіки порівняння, яку ми визначимо в реалізації Compare.

Чим відрізняється IComparer<T> від IComparable<T>

Клас / Інтерфейс Де реалізується Для чого потрібен Приклад використання
IComparable<T>
Безпосередньо в типі (класі/структурі) Один стандартизований спосіб порівняння Сортування за зростанням ID
IComparer<T>
В окремому класі Будь-яка кількість способів порівняння Сортування за імʼям, датою

3. Реалізація IComparer<T> на практиці

Продовжмо наш «один великий застосунок» — просту модель користувачів. Нехай у нас є такий клас:


// Наш клас користувача
public class User
{
    public string Name { get; set; }
    public int Age { get; set; }
    public string Email { get; set; }
}

Сортування за імʼям: реалізуємо окремий компаратор

Створімо окремий клас, що реалізує IComparer<User> і порівнюватиме користувачів за імʼям:


// Клас-компаратор для сортування за ім'ям
public class UserNameComparer : IComparer<User>
{
    public int Compare(User x, User y)
    {
        // Перевіряємо на null (щоб не було сюрпризів!)
        if (ReferenceEquals(x, y)) return 0;
        if (x is null) return -1;   // null "менше" будь-якого об'єкта
        if (y is null) return 1;

        // Порівнюємо за ім'ям (з урахуванням стандарту сортування рядків)
        return string.Compare(x.Name, y.Name, StringComparison.OrdinalIgnoreCase);
    }
}

Використовуємо компаратор для сортування списку:


List<User> users = new List<User>
{
    new User { Name = "Іван", Age = 20, Email = "ivan@mail.com" },
    new User { Name = "Анна", Age = 32, Email = "anna@gmail.com" },
    new User { Name = "Борис", Age = 28, Email = "boris@work.org" },
    new User { Name = "Руслан", Age = 19, Email = "ruslan@yandex.ru" }
};

// Сортуємо за ім'ям за допомогою IComparer
users.Sort(new UserNameComparer());

users.ForEach(u => Console.WriteLine(u.Name)); // Анна, Борис, Іван, Руслан

Бачите, як це елегантно? Клас‑компаратор стає окремим компонентом вашого застосунку — його можна використовувати для будь‑яких списків користувачів.

4. Кілька варіантів порівняння: створюємо різні компаратори

Можна створити стільки компараторів, скільки потрібно. Наприклад, реалізуймо сортування за віком:


// Компаратор для сортування за віком
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;

        // Сортуємо за зростанням віку
        return x.Age.CompareTo(y.Age);
    }
}

І тепер:


users.Sort(new UserAgeComparer());
users.ForEach(u => Console.WriteLine($"{u.Name} ({u.Age})"));
// Вивід: Руслан (19) Іван (20) Борис (28) Анна (32)

Якщо потрібно відсортувати за віком за спаданням, можна просто поміняти місцями аргументи:


// Компаратор для сортування за спаданням віку
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;

        // Міняємо порядок: y.CompareTo(x)
        return y.Age.CompareTo(x.Age);
    }
}

5. Корисні нюанси

Як це працює всередині?

Коли ви викликаєте Sort() із компаратором, список передає кожну пару елементів цьому компаратору й запитує: «Кого поставити раніше?». Ваш Compare відповідає: «цього», «того» або «порядок можна не змінювати». Сортування повторює цей діалог для необхідних пар, доки не отримає фінальний відсортований список.

Що робити, якщо значення рівні? Поверніть 0 — порядок елементів не зміниться (або визначиться внутрішнім механізмом сортування).

Де ще використовується IComparer<T>?

Інтерфейс IComparer<T> активно використовується не лише в списках. Ось кілька прикладів, де він трапляється в .NET:

  • У конструкторах таких колекцій, як SortedList<TKey, TValue> і SortedSet<T>: потрібно задати певний порядок елементів.
  • Для пошуку за допомогою BinarySearch.

Ось приклад:


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

Відтепер SortedSet автоматично підтримуватиме порядок за віком.

Коротко про null-безпечність

Одна з найчастіших помилок, із якими стикаються новачки, — NullReferenceException. Не забувайте перевіряти об’єкти на null усередині Compare, особливо якщо список може містити такі значення.

Типовий шаблон (наведемо ще раз для певності):


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

Це корисна звичка: вона допомагає уникнути падіння програми у найневдаліший момент!

Переваги та обмеження підходу з IComparer<T>

  • Дозволяє чітко відокремити логіку порівняння від даних. Клас користувача не зобов’язаний «знати», як і чому його будуть сортувати.
  • Дозволяє легко повторно використовувати логіку порівняння в різних місцях.
  • Забезпечує масштабованість: можна створювати скільки завгодно варіантів сортування без зміни вихідного типу.

Але будьте уважні, щоб не потрапити в ситуацію «забули про null» або «логіка порівняння неузгоджена». Наприклад, якщо Compare(x, y) повертає 0, то Compare(y, x) теж має повертати 0; якщо Compare(x, y) повертає > 0, то Compare(y, x) має повертати < 0 тощо.

Візуальна пам’ятка: коли що використовувати?

Задача Що використовувати Де реалізувати логіку
Один «природний» спосіб сортування
IComparable<T>
У самому типі (класі/структурі)
Різні варіанти сортування
IComparer<T>
В окремому класі
Швидко, одноразово, „на льоту“
Comparison<T>
/ лямбда
У параметрі методу Sort, через делегат
Складна, часто використовувана логіка
IComparer<T>
Як окремий клас‑компаратор

У наступній лекції ви познайомитеся з тим, як використовувати й комбінувати делегати та лямбда-вирази для порівняння об’єктів. А поки спробуйте для свого застосунку реалізувати кілька різних компараторів і відчуйте переваги елегантної архітектури, де сортування винесено за межі основного класу, а логіка вибору критерію залишається гнучкою та розширюваною.

6. Типові помилки при реалізації компараторів

Помилка № 1: відсутність перевірки на null.
Якщо один з об’єктів у порівнянні дорівнює null, а ви не передбачили це в коді, програма може впасти з NullReferenceException.

Помилка № 2: некоректні значення -1, 0, +1.
Метод Compare має повертати від’ємне число, якщо перший об’єкт менший за другий, нуль — якщо вони рівні, і додатне — якщо більший. Порушення цього правила призводить до непередбачуваної поведінки сортування.

Помилка № 3: асиметрична логіка порівняння.
Якщо при порівнянні x і y ви повертаєте одне значення, а при порівнянні y і x — те саме (замість протилежного), результат стає непередбачуваним.

Помилка № 4: використання Sort() без компаратора для користувацького типу.
Якщо тип не реалізує IComparable або IComparable<T>, то виклик Sort() без явного компаратора завершиться винятком InvalidOperationException.

Як уникнути:
Перевіряйте граничні випадки, покривайте критичні ділянки коду юніт‑тестами (ми до цього ще повернемося), не бійтеся звертатися до документації — і нехай компаратори працюють, як швейцарський годинник.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ