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 для колекцій виділяють кілька ключових інтерфейсів. Розгляньмо їх коротко — у наступних лекціях ви дізнаєтеся деталі реалізації кожного.
| Інтерфейс | Для чого потрібен | Приклад колекцій, які реалізують інтерфейс |
|---|---|---|
|
Перебирання елементів (foreach) | Усі: , , масиви |
|
Змінна колекція (Add, Remove, Count) | , , |
|
Доступ за індексом, модифікація | , масиви |
|
Робота з парами «ключ — значення» | , |
|
Множина (унікальні елементи) | , |
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> або порахувати їх вручну.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ