JavaRush /Курси /C# SELF /Роль інтерфейсів у колекціях C#

Роль інтерфейсів у колекціях C#

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

1. Навіщо колекціям інтерфейси?

Поки ви працювали з конкретним типом (List<int>, Dictionary<string, string>), усе було просто й зрозуміло. Але уявіть, що ви вирішили замінити List<T> на HashSet<T>, бо потрібні лише унікальні значення. Або створити метод, який міг би працювати і з одним, і з іншим типом колекцій. Що робити?

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

Це як зарядний пристрій для телефону: якщо є стандартний роз’єм USB-C, ви зможете заряджати і телефон, і сучасний пилосос, і навіть електричну зубну щітку (головне — не сплутати їх у темній кімнаті).

Приклад на практиці

Припустімо, ви хочете написати метод, який рахує суму всіх чисел у будь-якій колекції чисел. Так це виглядало б без інтерфейсів:


// Такий метод працює лише з масивами:
static int SumArray(int[] array)
{
    int sum = 0;
    for (int i = 0; i < array.Length; i++)
    {
        sum += array[i];
    }
    return sum;
} 

// А цей працює лише з List<int>:
static int SumList(List<int> list)
{
    int sum = 0;
    for (int i = 0; i < list.Count; i++)
        sum += list[i];
    return sum;
}

Незручно, адже код майже однаковий. За допомогою інтерфейсу IEnumerable<int> можна зробити універсальний метод:


static int SumAll(IEnumerable<int> collection)
{
    int sum = 0;
    foreach (var number in collection)
        sum += number;
    return sum;
}

Тепер цей метод приймає будь-який тип колекції, який реалізує IEnumerable<int>: масив, список, множину тощо.

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

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

Інтерфейс Для чого потрібен Приклад колекцій, які реалізують інтерфейс
IEnumerable<T>
Перебирання елементів (foreach) Усі:
List<T>
,
HashSet<T>
, масиви
ICollection<T>
Змінна колекція (Add, Remove, Count)
List<T>
,
HashSet<T>
,
Dictionary<K,V>
IList<T>
Доступ за індексом, модифікація
List<T>
, масиви
IDictionary<K,V>
Робота з парами «ключ — значення»
Dictionary<K,V>
,
SortedDictionary<K,V>
ISet<T>
Множина (унікальні елементи)
HashSet<T>
,
SortedSet<T>

P.S. Для простоти ми поки що опускаємо «кілька рівнів» успадкування між цими інтерфейсами. Хто від кого успадковує — про це трохи згодом.

Візуалізація: дерево інтерфейсів колекцій


           IEnumerable<T>
                 ^
                 |
          ICollection<T>
              ^       ^
              |       |
         IList<T>  ISet<T>
              |       |
         List<T>   HashSet<T>
Ієрархія інтерфейсів для списків і множин

Для словників:


           IEnumerable<KeyValuePair<K,V>>
                      ^
                      |
               ICollection<KeyValuePair<K,V>>
                      ^
                      |
               IDictionary<K,V>
                      |
                Dictionary<K,V>
Ієрархія інтерфейсів для словників

3. Навіщо інтерфейси у реальних проєктах?

Гнучкість

Коли ваш метод або клас працює не з конкретним типом (List<int>), а з інтерфейсом (IEnumerable<int>), ви можете передавати йому будь-які колекції, які реалізують цей інтерфейс. У результаті програма стає гнучкою та розширюваною. (Замість «тільки iPhone» — підійде будь-який телефон із Bluetooth.)


static void PrintAll(IEnumerable<string> collection)
{
    foreach (var item in collection)
        Console.WriteLine(item);
}

Тепер можна виводити елементи будь-якого списку, масиву, множини — навіть результату LINQ-запиту!

Уніфікація

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

Сумісність зі стандартними інструментами

Завдяки підтримці стандартних інтерфейсів ваші колекції можуть працювати з LINQ, сортуванням, серіалізацією та іншими можливостями .NET.

4. List<T> — це і список, і колекція, і перелік

Кілька слів про перелік

IEnumerable<T> — це найважливіший і базовий інтерфейс колекцій у .NET. Він визначає один-єдиний метод — GetEnumerator(), який повертає об’єкт-перелічувач (Enumerator) для ітерації елементами колекції. Саме завдяки цьому ви можете писати:


foreach (var name in names)
{
    Console.WriteLine(name);
}

Де names може бути і списком, і множиною, і навіть масивом. foreach — це синтаксичний цукор, який під капотом використовує метод GetEnumerator().

Інтерфейс IEnumerable (без <T>) — старіший, неузагальнений. Тепер майже завжди використовуйте узагальнену версію.

Синтаксис оголошення класу List

Погляньмо на оголошення класу List<T>, якщо трохи зазирнути під капот:


public class List<T> : IList<T>, ICollection<T>, IEnumerable<T>, ...

Це означає, що об’єкт List<T> можна використовувати як:

  • Список із довільним доступом за індексом (IList<T>)
  • Колекцію з можливістю додавати/видаляти елементи (ICollection<T>)
  • Просто набір елементів, яким можна пройтися (IEnumerable<T>)

Приклад:


List<string> names = new List<string> { "Анна", "Борис", "Василь" };

// Як IEnumerable: можна пройтися в циклі
IEnumerable<string> asEnumerable = names;
foreach (var n in asEnumerable) Console.WriteLine(n);

// Як ICollection: можна дізнатися Count:
ICollection<string> asCollection = names;
int count = asCollection.Count;

// Як IList: можна звернутися за індексом:
IList<string> asList = names;
string firstItem = asList[0];

5. Приклад застосування

Припустімо, у нас є простий застосунок обліку користувачів і їхніх ролей (ми почали його створювати в попередніх лекціях):


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

Ми можемо зберігати користувачів у будь-якій колекції, що реалізує IEnumerable<User>:


List<User> listUsers = new List<User> { new User { Name = "Анна", Age = 20 } };
HashSet<User> setUsers = new HashSet<User> { new User { Name = "Борис", Age = 25 } };

// Метод, який виводить імена всіх користувачів
static void PrintUserNames(IEnumerable<User> users)
{
    foreach (var user in users)
        Console.WriteLine(user.Name);
}

// Використовуємо з різними типами колекцій:
PrintUserNames(listUsers);
PrintUserNames(setUsers);

Тепер застосунок стає значно гнучкішим — без магії й кастомних перевантажень!

6. Типові помилки та підводні камені

Одна з найчастіших помилок — використання надто конкретних типів, коли достатньо інтерфейсу:


// Погано (жорстко прив’язалися до реалізації)
void DoSomethingWithList(List<int> numbers) { ... }

// Краще (метод працює з будь-якою колекцією)
void DoSomethingWithNumbers(IEnumerable<int> numbers) { ... }

Ще одна пастка — забути, що інтерфейс задає лише ті операції, які явно в ньому описані. Наприклад, у IEnumerable<T> не можна дізнатися кількість елементів (Count): для цього слід використовувати ICollection<T> або порахувати їх вручну.

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