1. Вступ
Якщо з IEnumerable<T> ми могли лише перебирати наявний вміст колекції, то інтерфейс ICollection<T> — це ваша перепустка у світ керування колекцією: додавати, видаляти, перевіряти кількість елементів, копіювати їх у масив і навіть стежити за змінами (майже як у реальному житті, коли ви намагаєтеся контролювати вміст свого кошика для покупок).
ICollection<T> реалізують усі змінювані колекції .NET, зокрема List<T>, HashSet<T>, Dictionary<TKey, TValue>.ValueCollection та навіть менш популярні, на кшталт Queue<T> і Stack<T> (так, навіть черги та стеки!). Це базовий контракт для всіх класів колекцій, які дозволяють модифікувати свій вміст.
Сімейство інтерфейсів колекцій
2. Що входить в інтерфейс ICollection<T>?
На відміну від IEnumerable<T>, який відповідає лише за перебирання (foreach), ICollection<T> — це мов швейцарський ніж серед інтерфейсів: для кожного завдання знайдеться метод!
Ось його головний вміст:
public interface ICollection<T> : IEnumerable<T>
{
int Count { get; }
bool IsReadOnly { get; }
void Add(T item);
void Clear();
bool Contains(T item);
void CopyTo(T[] array, int arrayIndex);
bool Remove(T item);
}
Список методів і властивостей:
- Count — кількість елементів у колекції. Аналог вашої улюбленої функції підрахунку.
- IsReadOnly — чи можна змінювати колекцію? Якщо true, колекція лише для читання (наприклад, у деяких обгортках).
- Add(T item) — додає елемент у колекцію. Один із ваших головних інструментів!
- Clear() — видаляє всі елементи. Повна зачистка.
- Contains(T item) — швидка перевірка: чи є такий елемент у колекції?
- CopyTo(T[] array, int arrayIndex) — копіює елементи у звичайний масив, починаючи з певного індексу. Корисно для передачі даних у зовнішні API або застарілі методи.
- Remove(T item) — видаляє елемент (якщо він там є).
3. Реальні завдання програміста
Чому важливо розуміти, як працює цей інтерфейс? На практиці ви часто пишете узагальнений код, який працює з будь-якими колекціями. Наприклад, функція може приймати будь-який список, чергу чи множину — і не має значення, який саме тип колекції було передано. Поки ця колекція реалізує ICollection<T>, ви зможете додавати й видаляти елементи, перевіряти розмір і навіть копіювати в масив. Це основа для гнучких бібліотек і універсальних алгоритмів.
Приклад із життя: інтерфейс ICollection<T> часто трапляється в параметрах методів і властивостях API, які повертають колекцію для змін. Наприклад, у популярних ORM (Entity Framework, Dapper) властивості типу ICollection<T> використовують для опису колекцій пов’язаних об’єктів.
Приклади використання
Спробуймо написати універсальний метод, який приймає будь-яку колекцію і додає до неї елемент. Головна умова — вона має реалізовувати ICollection<T>.
// Універсальний метод додавання елемента
void AppendItem<T>(ICollection<T> collection, T item)
{
collection.Add(item);
}
Тепер цим методом можна користуватися як із List<T>, так і з HashSet<T>, і навіть із вашою власною реалізацією інтерфейсу! Ось так виглядає використання:
var numbers = new List<int> { 1, 2, 3 };
AppendItem(numbers, 42);
var uniqueNames = new HashSet<string> { "Аліса", "Боб" };
AppendItem(uniqueNames, "Чарлі");
Навіть кумедно — нам усе одно, список перед нами чи множина, головне, що це ICollection<T>. Не дивуйтеся: саме так часто влаштовано універсальні методи у .NET!
4. Звʼязок з іншими колекціями
ICollection<T> — це не лише теоретично красивий контракт, а й реальний фундамент для майже всіх колекцій, з якими ви зіткнетеся у .NET.
Щоб побачити на практиці, наскільки він універсальний, спробуйте наведений нижче код:
void ShowCollectionInfo<T>(ICollection<T> collection)
{
Console.WriteLine($"Кількість елементів: {collection.Count}");
Console.WriteLine($"Чи можна змінювати: {!collection.IsReadOnly}");
Console.WriteLine("Елементи:");
foreach (var item in collection)
{
Console.WriteLine(item);
}
}
// Для List<T>
var list = new List<string> { "яблуко", "апельсин" };
ShowCollectionInfo(list);
// Для HashSet<T>
var set = new HashSet<string> { "яблуко", "апельсин" };
ShowCollectionInfo(set);
Результат буде однаково коректним для будь-якого типу колекції, що реалізує цей інтерфейс. Спробуйте для черги, стека, навіть для обгорнутого масиву!
5. Колекції лише для читання
Інтерфейс ICollection<T> містить властивість IsReadOnly, яка вказує, чи можна змінювати колекцію. Не плутайте з властивостями масивів! Якщо ви отримали колекцію, у якій IsReadOnly == true, спроба додати або видалити елемент спричинить виняток.
Приклад:
// Створимо масив і обгорнемо його в ReadOnlyCollection
var array = new int[] { 1, 2, 3 };
ICollection<int> readOnly = Array.AsReadOnly(array);
// Спробуємо додати елемент
try
{
readOnly.Add(4); // Буде кинуто NotSupportedException
}
catch (NotSupportedException)
{
Console.WriteLine("Не можна додавати елементи: колекція лише для читання!");
}
Це трапляється, наприклад, коли ви отримали колекцію із зовнішнього джерела й її не можна змінювати — зате можна читати елементи й дізнаватися їхню кількість.
6. Таблиця порівняння методів популярних колекцій крізь призму ICollection<T>
| Колекція | Add() | Remove() | Contains() | Clear() | CopyTo() | IsReadOnly |
|---|---|---|---|---|---|---|
|
Так | Так | Так | Так | Так | Ні |
|
Так* | Так | Так | Так | Так | Ні |
|
Ні/Ні | Ні/Ні | Ні/Ні | Так/Так | Так/Так | Ні |
|
Ні | Ні | Так | Ні | Так | Так |
|
Ні | Ні | Так | Ні | Так | Так/Ні* |
Для HashSet<T> метод Add повертає true, якщо елемент було додано, інакше false.
7. CopyTo — навіщо може знадобитися
Метод CopyTo дозволяє швидко перенести вміст колекції у звичайний масив. Буває корисно, якщо треба, наприклад, повернути дані у функцію застарілого API, яка приймає лише масиви, або виконати масову обробку засобами масиву.
var contactsArray = new Contact[book.Count];
((ICollection<Contact>)book).CopyTo(contactsArray, 0);
// Тепер можна використовувати contactsArray як і раніше
8. Приклад
У межах нашого навчального консольного застосунку, скажімо, ми почали розробляти адресну книгу. Нехай у нас є клас Contact, а адресна книга може бути реалізована на основі будь-якої колекції, аби вона підтримувала додавання, видалення й перебирання.
public class Contact
{
public string Name { get; set; }
public string Phone { get; set; }
}
public class AddressBook
{
private readonly ICollection<Contact> _contacts;
public AddressBook(ICollection<Contact> contacts)
{
_contacts = contacts;
}
public void AddContact(Contact contact)
{
_contacts.Add(contact);
}
public bool RemoveContact(Contact contact)
{
return _contacts.Remove(contact);
}
public int Count => _contacts.Count;
public void PrintAll()
{
foreach (var contact in _contacts)
{
Console.WriteLine($"{contact.Name} — {contact.Phone}");
}
}
}
Тепер наша адресна книга може працювати як із List<Contact>, так і з HashSet<Contact>, і навіть — у майбутньому — з власною колекцією, яку ми реалізуємо на основі бази даних або файлів! Приклад використання:
var book = new AddressBook(new List<Contact>());
book.AddContact(new Contact { Name = "Іван", Phone = "+38-123-456" });
book.AddContact(new Contact { Name = "Марія", Phone = "+38-987-654" });
Console.WriteLine($"Усього контактів: {book.Count}");
book.PrintAll();
Якщо хочемо позбутися дублікатів, можемо просто передати в AddressBook замість списку множину:
var book = new AddressBook(new HashSet<Contact>());
(потрібно, щоб Contact коректно реалізував порівняння, але про це буде окрема лекція!)
9. Особливості методів і типові помилки
Одразу зверніть увагу: не всі колекції, що реалізують ICollection<T>, поводяться однаково! Наприклад, спроба змінити колекцію, коли вона IsReadOnly == true, спричинить виняток. А якщо ви працюєте з HashSet<T>, то метод Add поверне true лише тоді, коли елемент справді було додано (до цього він був відсутній). Крім того, у різних колекцій реалізація методів може відрізнятися за швидкістю — однак інтерфейс цього не регламентує.
Ще один поширений момент: якщо колекція використовується кількома потоками, зміна колекції в одному потоці й перебирання в іншому можуть призвести до винятків. Для багатопотокових сценаріїв потрібні спеціальні колекції (ConcurrentBag<T>, ConcurrentQueue<T> та ін. — про це згодом).
Ще одна пастка: якщо ви скопіювали колекцію методом CopyTo, то далі зміни в масиві й у колекції будуть незалежні одна від одної. Масив — це окрема область пам’яті.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ