JavaRush /Курси /C# SELF /Контракт для колекції: ICo...

Контракт для колекції: ICollection<T>

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

1. Вступ

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

ICollection<T> реалізують усі змінювані колекції .NET, зокрема List<T>, HashSet<T>, Dictionary<TKey, TValue>.ValueCollection та навіть менш популярні, на кшталт Queue<T> і Stack<T> (так, навіть черги та стеки!). Це базовий контракт для всіх класів колекцій, які дозволяють модифікувати свій вміст.

Сімейство інтерфейсів колекцій

ICollection<T> розширює IEnumerable<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
List<T>
Так Так Так Так Так Ні
HashSet<T>
Так* Так Так Так Так Ні
Queue<T> / Stack<T>
Ні/Ні Ні/Ні Ні/Ні Так/Так Так/Так Ні
ReadOnlyCollection<T>
Ні Ні Так Ні Так Так
Dictionary<TKey, TValue>.ValueCollection
Ні Ні Так Ні Так Так/Ні*

Для 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, то далі зміни в масиві й у колекції будуть незалежні одна від одної. Масив — це окрема область пам’яті.

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