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
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 |
|---|---|---|---|---|---|---|
|
Да | Да | Да | Да | Да | Нет |
|
Да* | Да | Да | Да | Да | Нет |
|
Нет/Нет | Нет/Нет | Нет/Нет | Да/Да | Да/Да | Нет |
|
Нет | Нет | Да | Нет | Да | Да |
|
Нет | Нет | Да | Нет | Да | Да/Нет* |
Для 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, то далее изменения в массиве и в коллекции будут независимы друг от друга. Массив — это отдельная область памяти.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ