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> (да-да, даже очереди и стеки!). Это базовый контракт для всех классов коллекций, которые позволяют модифицировать своё содержимое.

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

graph TD
    A[IEnumerable<T>]
    B[ICollection<T>]
    C[IList<T>]
    D[IDictionary<TKey, TValue>]
    E[Queue<T>, Stack<T>, List<T>, HashSet<T>...]

    A --> B
    B --> C
    B --> D
    B --> E
    
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> { "Alice", "Bob" };
AppendItem(uniqueNames, "Charlie");

Даже смешно — нам не важно, список перед нами или множество, главное что это 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 = "+7-123-456" });
book.AddContact(new Contact { Name = "Мария", Phone = "+7-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, то далее изменения в массиве и в коллекции будут независимы друг от друга. Массив — это отдельная область памяти.

2
Задача
C# SELF, 28 уровень, 2 лекция
Недоступна
Создание и модификация коллекции с использованием ICollection<T>
Создание и модификация коллекции с использованием ICollection<T>
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ