JavaRush /Курси /C# SELF /Практичне застосування компараторів у .NET

Практичне застосування компараторів у .NET

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

1. Вступ

Чому компаратори — це справді важливо…

У реальних проєктах усе не так просто, як здається в навчальних завданнях: об’єкти складні, дані надходять від користувачів або інших сервісів, а потрібні нам «порядки сортування» (або навіть правила рівності) часто змінюються від завдання до завдання. Компаратори дають змогу зробити код гнучким, а поведінку — передбачуваною й контрольованою.

У .NET компаратори потрібні щоразу, коли слід сортувати, шукати, групувати або видаляти дублікати об’єктів власних класів, а ще — коли ви будуєте структури даних із «порядком». Особливо часто це трапляється у колекціях, алгоритмах і під час інтеграції із зовнішніми API.

Де зустрічаються компаратори в .NET:

  • Сортування колекцій (List<T>.Sort, Array.Sort, OrderBy у LINQ)
  • Структури даних із впорядкуванням (SortedSet<T>, SortedDictionary<TKey, TValue>)
  • Пошук і порівняння об’єктів у колекціях (Contains, IndexOf — коли потрібно визначати «рівність», а не лише порядок)
  • Групування, фільтрація та усунення дублікатів (наприклад, .Distinct())

Навіщо це потрібно на практиці?

  • Звіти, де важливе відсортоване виведення (наприклад, список студентів за абеткою або за середнім балом)
  • Зіставлення вхідних даних з еталонними значеннями (наприклад, пошук запису за певним критерієм)
  • Економія пам’яті та підвищення продуктивності (правильний вибір структури даних пришвидшує пошук і робить застосунок відзивнішим)
  • Перевірка унікальності (наприклад, під час реєстрації користувача за e-mail)

2. Приклад із життя: сортування за різними критеріями

Припустімо, у нас є клас користувача:

public class User
{
    public string FirstName { get; set; } = "";
    public string LastName { get; set; } = "";
    public string Email { get; set; } = "";
    public int Age { get; set; }

    // Можна додати перевизначення Equals і GetHashCode — але це для самостійного ознайомлення
}

У нас є список користувачів, і ми хочемо:

  • Відсортувати їх за прізвищем, а якщо прізвища збігаються — за ім’ям.
  • Надати можливість шукати користувача за e-mail (ігноруючи регістр).
  • Вилучати дублікати користувачів під час додавання.

Сортування за допомогою IComparer<T>

Варіант 1: класичний спосіб — створити окремий компаратор.

// Компаратор для сортування за прізвищем та ім’ям
public class UserFullNameComparer : 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;

        int lastNameComparison = StringComparer.OrdinalIgnoreCase.Compare(x.LastName, y.LastName);
        if (lastNameComparison != 0)
            return lastNameComparison;

        return StringComparer.OrdinalIgnoreCase.Compare(x.FirstName, y.FirstName);
    }
}

Використання:

var users = new List<User>
{
    new User { FirstName = "Іван", LastName = "Петров", Age = 20, Email = "ivan.petrov@email.com" },
    new User { FirstName = "Анна", LastName = "Смирнова", Age = 22, Email = "anna.smirnova@email.com" },
    new User { FirstName = "Петро", LastName = "Петров", Age = 18, Email = "petr.petrov@email.com" }
};

users.Sort(new UserFullNameComparer());

foreach (var user in users)
{
    Console.WriteLine($"{user.LastName} {user.FirstName} ({user.Age})");
}

// Результат — користувачі відсортовані за прізвищем; якщо прізвище однакове — за ім’ям.

Зверніть увагу: Так роблять, якщо порядок порівняння потрібен у кількох різних місцях застосунку і якщо лямбда-вираз уже не рятує від дублювання коду.

Сортування «на льоту» за допомогою лямбди

Не хочете створювати окремі класи заради одноразового порядку? Допоможе лямбда-вираз!

users.Sort((x, y) =>
{
    int lastNameComparison = StringComparer.OrdinalIgnoreCase.Compare(x.LastName, y.LastName);
    if (lastNameComparison != 0)
        return lastNameComparison;
    return StringComparer.OrdinalIgnoreCase.Compare(x.FirstName, y.FirstName);
});

Працює аналогічно, але компаратор створюється просто всередині виклику. Економить рядки, втім не завжди зручно для багаторазового використання.

3. Хитрий пошук: порівняння e-mail без урахування регістру

У реальному житті користувачі вводять e-mail як завгодно, а ви — програміст, а не суддя, тому доречно порівнювати e-mail без урахування регістру.

Реалізуємо це за допомогою компаратора рівності та пошуку:

public class EmailComparer : IEqualityComparer<User>
{
    public bool Equals(User? x, User? y)
    {
        if (ReferenceEquals(x, y)) return true;
        if (x is null || y is null) return false;
        return string.Equals(x.Email, y.Email, StringComparison.OrdinalIgnoreCase);
    }
    public int GetHashCode(User obj)
    {
        return obj.Email?.ToLowerInvariant().GetHashCode() ?? 0;
    }
}

Використання в HashSet:

var usersSet = new HashSet<User>(new EmailComparer());
usersSet.Add(new User { Email = "Petrov@example.com" });
bool contains = usersSet.Contains(new User { Email = "petrov@example.com" }); // true!

Важливе зауваження: Коли створюєте свій EqualityComparer, не забудьте реалізувати обидва методи: Equals і GetHashCode. Якщо пропустите другий — отримаєте дивну поведінку: пошук у колекціях може працювати некоректно.

4. Застосування SortedSet і SortedDictionary

Тут компаратори справді розкриваються.

SortedSet<T> і SortedDictionary<TKey, TValue> не зможуть працювати з вашими об’єктами, якщо ви не поясните .NET, як їх порівнювати. Порядок порівняння та визначення рівності впливають на те, які елементи вважаються різними!

Приклад із SortedSet<User>

var sortedUsersByFullName = new SortedSet<User>(new UserFullNameComparer())
{
    new User { FirstName = "Іван", LastName = "Петров", Age = 20 },
    new User { FirstName = "Анна", LastName = "Смирнова", Age = 22 },
    new User { FirstName = "Петро", LastName = "Петров", Age = 18 },
    new User { FirstName = "Іван", LastName = "Петров", Age = 25 } // Дубль за ім’ям і прізвищем
};

// «Іван Петров» із різним віком — лише один потрапить у множину
Console.WriteLine("Користувачі в SortedSet:");
foreach (var user in sortedUsersByFullName)
    Console.WriteLine($"{user.LastName} {user.FirstName} ({user.Age})");

SortedSet не додасть двох «Іванів Петрових»!

Важливо: Компаратор тут визначає логіку «унікальності». Якщо ви порівнюєте лише за прізвищем та ім’ям, то користувачі з однаковими ПІБ, але різним віком вважаються одним і тим самим користувачем.

5. Таблиця: коли який компаратор у .NET використовувати

Сценарій Що реалізувати Приклад використання
Природний порядок сортування (один, універсальний для типу)
IComparable<T>
List<T>.Sort()
Кілька варіантів сортування (за ім’ям, віком, e-mail…)
IComparer<T>
(класи, лямбди)
List<T>.Sort(comparer)
Пошук унікальних елементів у колекції
IEqualityComparer<T>
HashSet<T>
,
Dictionary<TKey, TValue>
Групування, видалення дублікатів
IEqualityComparer<T>
LINQ
.Distinct(comparer)
Сортування за часом, датою або складною комбінацією полів
Comparison<T>
, лямбда «на льоту»
List<T>.Sort((a, b) => ...)
Використання в LINQ (разові запити) лямбда-вираз у
OrderBy
,
ThenBy
OrderBy(x => x.Name)

6. Практика: пошук і порівняння в колекціях

Давайте реалізуємо у нашому застосунку реєстрацію користувача з унікальним e-mail (без урахування регістру). Якщо користувача з такою адресою вже додано, слід повідомити про це.

public static bool RegisterUser(List<User> users, User newUser)
{
    // Використовуємо Any з лямбдою для перевірки унікальності адреси електронної пошти
    bool exists = users.Any(u =>
        string.Equals(u.Email, newUser.Email, StringComparison.OrdinalIgnoreCase));
    if (exists)
    {
        Console.WriteLine($"Користувача з e-mail {newUser.Email} уже зареєстровано!");
        return false;
    }
    users.Add(newUser);
    Console.WriteLine($"Користувача {newUser.FirstName} додано.");
    return true;
}

Використання:

var userList = new List<User>
{
    new User { Email = "first@example.com" }
};

RegisterUser(userList, new User { FirstName = "Вася", Email = "FIRST@example.com" });
// Виведе: "Користувача з e-mail FIRST@example.com уже зареєстровано!"

7. Типові помилки при використанні компараторів

Усі люблять переліки помилок, але ми подамо їх живіше.

Іноді, коли програміст реалізує компаратор для власного типу, він забуває перевірити на null або — що ще гірше — реалізує порівняння так, що порушується «строгість порядку». Наприклад, якщо в компараторі повертати суперечливі значення, то сортування поводитиметься непередбачувано, а в колекціях об’єкти почнуть втрачатися або «склеюватись».

Ще одна часта плутанина — неузгодженість реалізації Equals і логіки компаратора. Наприклад, якщо Equals вважає, що об’єкти різні, а компаратор — що однакові, то в SortedSet або SortedDictionary пануватиме хаос: елемент не знаходиться, хоча, здається, він там є.

Також трапляються ситуації, коли програміст порівнює лише одну з ознак об’єкта (наприклад, прізвище), забуваючи, що інших користувачів із таким самим прізвищем може бути багато. У результаті об’єкти «затираються», губляться, і дані стають неконсистентними. Тобто вони вже не відображають реальний стан системи — з’являється дублювання, зникнення потрібної інформації або порушення логіки роботи застосунку.

1
Опитування
Компаратори, рівень 30, лекція 4
Недоступний
Компаратори
Компаратори та порівняння об'єктів
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ