JavaRush /Курси /C# SELF /Інтерфейси у стандартній бібліотеці .NET

Інтерфейси у стандартній бібліотеці .NET

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

1. Класифікація ключових інтерфейсів

Якщо ви думаєте, що інтерфейси — це іграшка для архітекторів і «чистого коду», а в реальних задачах без них можна обійтися, поспішаю вас запевнити: будь-який серйозний проєкт на C# ґрунтовно спирається на інтерфейси. Чому? Адже практично кожна частина стандартної бібліотеки .NET побудована через інтерфейси. Без них не буде ані роботи з колекціями, ані читання файлів, ані навіть фільтрування колекцій із LINQ.

Інтерфейси — це фундамент поліморфізму й розширюваності в .NET, саме вони визначають, як усі «блоки» фреймворку поєднуються між собою.

Стандартна бібліотека .NET щільно насичена інтерфейсами. Щоб не загубитися в цьому морі, пропоную таку класифікацію (звісно, вона неповна — життя не вистачить, щоб охопити все):

Категорія Інтерфейси Для чого потрібні?
Колекції
IEnumerable
,
IEnumerator
,
IList
,
ICollection
,
IDictionary
Перебирання, змінювання, доступ за індексом, робота з парами ключ–значення
Робота з ресурсами
IDisposable
Звільнення ресурсів (файли, з’єднання, потоки)
Порівняння
IComparable
,
IComparer
,
IEquatable
Упорядкування, порівняння, унікальність об’єктів
Серіалізація
ISerializable
Перетворення об’єктів на потік байтів (і навпаки)
LINQ і запити
IQueryable
,
IQueryProvider
Підтримка складних запитів (наприклад, до БД)
Асинхронність
IAsyncEnumerable
,
IAsyncDisposable
Асинхронне перебирання та очищення ресурсів
Події та сповіщення
INotifyPropertyChanged
,
INotifyCollectionChanged
Реагування на зміни властивостей і колекцій
Дата і час
IFormattable
Довільне форматування рядків
Структури даних
IStructuralComparable
,
IStructuralEquatable
Глибоке порівняння колекцій і кортежів
Потоки та ввід/вивід
IStream
,
IAsyncDisposable
,
IObserver
,
IObservable
Робота з потоками, push/pull-сповіщення

Важливо! Багато з вищезгаданих інтерфейсів мають параметри типу: List<string>. Зазвичай їх використовують у колекціях і для інтерфейсів колекцій Int[] → List<int>. Детальніше ми їх розберемо за кілька рівнів, коли знайомитимемося з колекціями.

2. Інтерфейси колекцій

IEnumerable і IEnumerator — ваша перепустка до foreach

Практично кожна колекція в .NET реалізує інтерфейс IEnumerable або навіть його узагальнену версію IEnumerable<T>. Саме цей інтерфейс дозволяє використовувати зручний синтаксис foreach:

List<int> numbers = new List<int> { 1, 2, 3 };
foreach (int n in numbers) // працює, бо List<int> реалізує IEnumerable<int>
{
    Console.WriteLine(n);
}

Ось так цей інтерфейс виглядає у мінімалістичній версії:

public interface IEnumerable
{
    IEnumerator GetEnumerator();
}

IEnumerator — це інтерфейс власне «перелічувача», якому довірено іти вашою колекцією:

public interface IEnumerator
{
    bool MoveNext();
    object Current { get; }
    void Reset();
}

У реальних задачах ви дуже часто передаватимете параметри й повертатимете значення типу IEnumerable<T>. Наприклад, метод, що повертає всі парні числа з масиву:

public IEnumerable<int> GetEvenNumbers(int[] array)
{
    foreach (var x in array)
        if (x % 2 == 0) yield return x;
}

Так, ключове слово yield бере реалізацію на себе — але про це детальніше пізніше.

ICollection і IList — колекції з доступом за індексом і змінюванням

Якщо вам потрібен не просто перебір, а можливість додавати або видаляти елементи чи навіть отримувати їх за індексом, застосовуйте більш спеціалізовані інтерфейси:

  • ICollection<T> — додає методи Add, Remove, а також властивість Count.
  • IList<T> — розширює список можливих операцій, зокрема доступ за індексом (this[int index]).
public void PrintFirstItem(IList<string> list)
{
    if (list.Count > 0)
        Console.WriteLine(list[0]);
}

List<T> реалізує і IEnumerable<T>, і ICollection<T>, і IList<T>. Це дуже зручно — можна працювати з ним як із простим переліком або скористатися повним набором методів.

IDictionary<TKey, TValue> — пари ключ–значення

Якщо вам потрібно працювати зі словниками (а це трапляється дуже часто), використовуйте інтерфейс IDictionary<TKey, TValue>. Він гарантує роботу з парами ключ–значення та доступ до значення за ключем.

public void PrintAllPairs(IDictionary<string, int> ages)
{
    foreach (var pair in ages)
        Console.WriteLine($"{pair.Key}: {pair.Value}");
}

Тут ages може бути будь-чим: хоч Dictionary<string, int>, хоч SortedDictionary<string, int> — головне, щоб вони реалізовували цей інтерфейс.

3. Інтерфейс IDisposable: правильна робота з ресурсами

Увесь ввід/вивід, робота з файлами, мережевими з’єднаннями, базами даних у .NET зав’язані на інтерфейс IDisposable. Цей інтерфейс визначає життєво важливий контракт: якщо об’єкт має некеровані ресурси, його потрібно «очистити» після використання. Саме завдяки цьому інтерфейсу можливий синтаксис using:

using (StreamReader reader = new StreamReader("file.txt"))
{
    // Працюємо з файлом, а після using файл гарантовано закривається!
    string line = reader.ReadLine();
}

Як влаштований сам інтерфейс? Елементарно:

public interface IDisposable
{
    void Dispose();
}

Проте практично кожна серйозна бібліотека його реалізує. Докладніше про належні практики роботи з цим інтерфейсом — в офіційній документації.

4. Інтерфейси для порівняння та унікальності

IComparable<T> і IComparer<T>

Якщо ви хочете, щоб колекція ваших об’єктів могла бути відсортована, об’єкти мають уміти порівнюватися між собою. Для цього існує інтерфейс IComparable<T>:

public class Student : IComparable<Student>
{
    public string Name { get; set; }
    public int Score { get; set; }

    public int CompareTo(Student? other)
    {
        // Сортування за спаданням балів
        if (other == null) return 1;
        return other.Score.CompareTo(this.Score);
    }
}

// Тепер можна сортувати студентів:
var students = new List<Student> { ... };
students.Sort();

Докладніше — у документації Microsoft.

IComparer<T> дозволяє визначити порівняння поза класом. Наприклад, інколи ви хочете сортувати студентів за ім’ям, а інколи — за балами:

public class NameComparer : IComparer<Student>
{
    public int Compare(Student? x, Student? y)
    {
        return string.Compare(x?.Name, y?.Name);
    }
}

IEquatable<T> — порівняння на рівність

Хочете, щоб ваш об’єкт брав участь у HashSet<T> (тобто вважався «унікальним» для колекцій)? Реалізуйте IEquatable<T>:

public class Person : IEquatable<Person>
{
    public string Name { get; set; }
    public bool Equals(Person? other)
    {
        if (other == null) return false;
        return this.Name == other.Name;
    }
}

5. Інтерфейси для подій та сповіщень

Хочете, щоб програма «дізнавалася», що в об’єкта змінилися властивості? Наприклад, щоб інтерфейс автоматично оновлювався, коли користувач змінює прізвище? Саме для цього існує інтерфейс INotifyPropertyChanged. Він дуже популярний у застосунках із графічним інтерфейсом (наприклад, WPF чи Xamarin).

Сигнатура інтерфейсу проста:

public interface INotifyPropertyChanged
{
    event PropertyChangedEventHandler? PropertyChanged;
}

Приклад реалізації:

public class User : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler? PropertyChanged;

    private string name;
    public string Name
    {
        get => name;
        set
        {
            if (name != value)
            {
                name = value;
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Name)));
            }
        }
    }
}

Тепер будь-які прив’язки інтерфейсу користувача до цього об’єкта автоматично оновлюватимуться під час зміни Name. Якщо зацікавилися — зверніться до офіційної документації.

6. Інші корисні інтерфейси

Інтерфейс IFormattable: гнучке форматування

Коли ви хочете, щоб ваш об’єкт міг гарно й по-різному виводитися у рядок (наприклад, із різною кількістю знаків після коми), реалізуйте інтерфейс IFormattable:

public class Temperature : IFormattable
{
    public double Celsius { get; }
    public Temperature(double celsius) => Celsius = celsius;

    public string ToString(string? format, IFormatProvider? formatProvider)
    {
        // Не будемо ускладнювати, просто покажемо 'C' чи 'F' залежно від формату
        if (format == "F")
            return $"{Celsius * 9 / 5 + 32} F";
        return $"{Celsius} C";
    }
}

Тепер ви можете викликати temp.ToString("F", null) або temp.ToString("C", null). Ось чому DateTime, double, decimal та інші вбудовані типи підтримують гнучке форматування.

Інтерфейси для асинхронності

З появою асинхронності в C# знадобилися нові інтерфейси для роботи в асинхронному світі. Наприклад, якщо ваш об’єкт асинхронно звільняє ресурси — реалізуйте IAsyncDisposable:

public interface IAsyncDisposable
{
    ValueTask DisposeAsync();
}

Тепер замість using ви пишете await using.

З асинхронними перерахуваннями (await foreach) працює інтерфейс IAsyncEnumerable<T>, який дозволяє перебирати елементи, не блокуючи основний потік. Його використовують, наприклад, під час читання великих файлів частинами або під час роботи з потоками даних з інтернету. Докладніше — на рівні 58.

Інтерфейси серіалізації: ISerializable

Якщо ваше завдання — перетворювати об’єкти на потік байтів для збереження чи передавання (наприклад, мережею), .NET пропонує інтерфейс ISerializable. Його використовують не так часто сьогодні, адже з’явилися зручніші механізми (наприклад, через атрибути та готові серіалізатори), але згадати варто. Ось приклад сигнатури:

public interface ISerializable
{
    void GetObjectData(SerializationInfo info, StreamingContext context);
}

7. Застосування інтерфейсів на практиці: реальний застосунок

Уявімо, ви створюєте консольний застосунок для обліку книжок у бібліотеці. Хочете мати змогу виводити списки книжок, сортувати їх, фільтрувати, завантажувати й зберігати дані. Завдяки знанню інтерфейсів .NET ви зможете:

  • Повернути різні типи колекцій через IEnumerable<Book>, щоб не доводилося зважати, масив це чи список.
  • Додати сортування за різними критеріями: реалізувати IComparable<Book> для сортування за назвою та окремий IComparer<Book> — за автором.
  • Забезпечити ефективне звільнення ресурсів під час роботи з файлами через IDisposable.
  • Організувати фільтрацію книжок за жанром чи автором за допомогою LINQ, який працює з усім, що реалізує IEnumerable<T>.
  • Масштабувати застосунок — наприклад, замінити файлове сховище на під’єднання до бази даних, якщо ваші класи працюють через інтерфейси (IBookStorage), а не через конкретні реалізації.

8. Типові помилки та особливості

Одна з найпоширеніших помилок — оголошувати змінні й параметри методів із конкретними реалізаціями («List») замість інтерфейсів («IEnumerable», «IList»). Пам’ятайте: коли ви «програмуєте на інтерфейсах», код стає гнучким, і його простіше розширювати.

Іноді варто уважно обирати рівень інтерфейсу: наприклад, якщо методу потрібен лише перебір, використовуйте IEnumerable<T>, а не IList<T>, щоб не нав’язувати зайвих обмежень.

Ще одна тонкість: якщо у класі багато інтерфейсів і між ними є методи чи властивості, що збігаються, доведеться використовувати явну реалізацію інтерфейсу (explicit implementation), інакше компілятор вас не зрозуміє.

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