1. Wprowadzenie
Jeśli z IEnumerable<T> mogliśmy tylko przechodzić po już istniejącej zawartości kolekcji, to interfejs ICollection<T> — to twój bilet do świata zarządzania kolekcją: możesz dodawać, usuwać, sprawdzać ilość elementów, kopiować je do tablicy, a nawet śledzić zmiany (no, prawie jak w prawdziwym życiu, kiedy próbujesz kontrolować zawartość swojego koszyka na zakupy).
ICollection<T> jest implementowany przez wszystkie modyfikowalne kolekcje .NET, takie jak List<T>, HashSet<T>, Dictionary<TKey, TValue>.ValueCollection i nawet mniej popularne jak Queue<T>, Stack<T> (tak, nawet kolejki i stosy!). To podstawowy kontrakt dla wszystkich klas kolekcji, które pozwalają modyfikować swoją zawartość.
Rodzina interfejsów kolekcji
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. Co zawiera interfejs ICollection<T>?
W odróżnieniu od IEnumerable<T>, który odpowiada tylko za iterację (foreach), ICollection<T> — to taki szwajcarski scyzoryk wśród interfejsów: na każdą okazję znajdziesz metodę!
Oto jego główna zawartość:
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);
}
Lista metod i właściwości:
- Count — ilość elementów w kolekcji. Odpowiednik twojej ulubionej funkcji liczenia.
- IsReadOnly — czy można zmieniać kolekcję? Jeśli true, kolekcja jest tylko do odczytu (np. dla niektórych wrapperów).
- Add(T item) — dodaje element do kolekcji. Jeden z twoich głównych narzędzi!
- Clear() — usuwa wszystkie elementy. Totalne czyszczenie.
- Contains(T item) — szybka kontrola: czy taki element jest w kolekcji?
- CopyTo(T[] array, int arrayIndex) — kopiuje elementy do zwykłej tablicy, zaczynając od określonego indeksu. Przydatne do przekazywania danych do zewnętrznych API lub "starych" metod.
- Remove(T item) — usuwa element (jeśli tam jest).
3. Prawdziwe zadania programisty
Dlaczego warto rozumieć, jak ten interfejs działa? W praktyce często piszesz generyczny kod, który działa z dowolnymi kolekcjami. Na przykład funkcja może przyjmować dowolną listę, kolejkę lub zbiór — i nie ma znaczenia, jaki konkretnie typ kolekcji został przekazany. Dopóki ta kolekcja implementuje ICollection<T>, możesz dodawać, usuwać elementy, sprawdzać rozmiar i nawet kopiować do tablicy. To podstawa dla elastycznych bibliotek i uniwersalnych algorytmów.
Przykład z życia: interfejs ICollection<T> często pojawia się w parametrach metod i właściwościach API, które zwracają kolekcję do modyfikacji. Na przykład w popularnych ORM (Entity Framework, Dapper) właściwości typu ICollection<T> są używane do opisu kolekcji powiązanych obiektów.
Przykłady użycia
Spróbujmy napisać uniwersalną metodę, która przyjmuje dowolną kolekcję i dodaje tam element. Główny warunek — musi implementować ICollection<T>.
// Uniwersalna metoda dodawania elementu
void AppendItem<T>(ICollection<T> collection, T item)
{
collection.Add(item);
}
Teraz tej metody możesz używać zarówno z List<T>, jak i z HashSet<T>, a nawet z własną implementacją interfejsu! Tak wygląda użycie:
var numbers = new List<int> { 1, 2, 3 };
AppendItem(numbers, 42);
var uniqueNames = new HashSet<string> { "Alice", "Bob" };
AppendItem(uniqueNames, "Charlie");
Nawet śmieszne — nie obchodzi nas, czy to lista czy zbiór, ważne że to ICollection<T>. Nie zdziw się, tak często są zbudowane uniwersalne metody w .NET!
4. Powiązanie z innymi kolekcjami
ICollection<T> — to nie tylko teoretycznie ładny kontrakt, ale i prawdziwy fundament dla praktycznie wszystkich kolekcji, z którymi spotkasz się w .NET.
Aby zobaczyć w praktyce, jak bardzo jest uniwersalny, spróbuj poniższego kodu:
void ShowCollectionInfo<T>(ICollection<T> collection)
{
Console.WriteLine($"Ilość elementów: {collection.Count}");
Console.WriteLine($"Czy można zmieniać: {!collection.IsReadOnly}");
Console.WriteLine("Elementy:");
foreach (var item in collection)
{
Console.WriteLine(item);
}
}
// Dla List<T>
var list = new List<string> { "jabłko", "pomarańcza" };
ShowCollectionInfo(list);
// Dla HashSet<T>
var set = new HashSet<string> { "jabłko", "pomarańcza" };
ShowCollectionInfo(set);
Wynik będzie poprawny dla dowolnego typu kolekcji implementującej ten interfejs. Spróbuj dla kolejki, stosu, nawet dla opakowanej tablicy!
5. Kolekcje tylko do odczytu
Interfejs ICollection<T> zawiera właściwość IsReadOnly, która wskazuje, czy można modyfikować kolekcję. Nie myl z typową właściwością dla tablic! Jeśli spotkasz kolekcję, w której IsReadOnly == true, próba dodania lub usunięcia elementu skończy się wyjątkiem.
Przykład:
// Stwórzmy tablicę i opakujmy ją w ReadOnlyCollection
var array = new int[] { 1, 2, 3 };
ICollection<int> readOnly = Array.AsReadOnly(array);
// Spróbujmy dodać element
try
{
readOnly.Add(4); // Zostanie rzucony NotSupportedException
}
catch (NotSupportedException)
{
Console.WriteLine("Nie można dodawać elementów: kolekcja tylko do odczytu!");
}
To się zdarza np. gdy otrzymałeś kolekcję z zewnętrznego źródła i nie możesz jej zmieniać — ale możesz czytać elementy i sprawdzać ich ilość.
6. Tabela porównawcza metod popularnych kolekcji przez pryzmat ICollection<T>
| Kolekcja | Add() | Remove() | Contains() | Clear() | CopyTo() | IsReadOnly |
|---|---|---|---|---|---|---|
|
Tak | Tak | Tak | Tak | Tak | Nie |
|
Tak* | Tak | Tak | Tak | Tak | Nie |
|
Nie/Nie | Nie/Nie | Nie/Nie | Tak/Tak | Tak/Tak | Nie |
|
Nie | Nie | Tak | Nie | Tak | Tak |
|
Nie | Nie | Tak | Nie | Tak | Tak/Nie* |
Dla HashSet<T> metoda Add zwraca true, jeśli element został dodany, w przeciwnym razie false.
7. CopyTo — po co może się przydać
Metoda CopyTo pozwala szybko przenieść zawartość kolekcji do zwykłej tablicy. Bywa przydatna, jeśli trzeba np. zwrócić dane do funkcji starego API, która przyjmuje tylko tablice, albo zrobić masową obróbkę narzędziami tablicowymi.
var contactsArray = new Contact[book.Count];
((ICollection<Contact>)book).CopyTo(contactsArray, 0);
// Teraz możesz używać contactsArray po staremu
8. Przykład
W ramach naszej naukowej aplikacji konsolowej, załóżmy, że zaczęliśmy tworzyć książkę adresową. Mamy klasę Contact, a książka adresowa może być oparta na dowolnej kolekcji, byleby wspierała dodawanie, usuwanie i iterację.
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}");
}
}
}
Teraz nasza książka adresowa może działać zarówno z List<Contact>, jak i z HashSet<Contact>, a nawet — w przyszłości — z własną kolekcją, którą zaimplementujemy na bazie bazy danych lub plików! Przykład użycia:
var book = new AddressBook(new List<Contact>());
book.AddContact(new Contact { Name = "Ivan", Phone = "+7-123-456" });
book.AddContact(new Contact { Name = "Maria", Phone = "+7-987-654" });
Console.WriteLine($"Wszystkich kontaktów: {book.Count}");
book.PrintAll();
Jeśli chcemy pozbyć się duplikatów, możemy po prostu przekazać do AddressBook zamiast listy zbiór:
var book = new AddressBook(new HashSet<Contact>());
(wymagane, żeby Contact poprawnie implementował porównanie, ale o tym będzie osobny wykład!)
9. Cechy metod i typowe błędy
Od razu zwracam uwagę: nie wszystkie kolekcje implementujące ICollection<T> zachowują się tak samo! Na przykład próba zmiany kolekcji, gdy IsReadOnly == true, skończy się wyjątkiem. A jeśli pracujesz z HashSet<T>, to metoda Add zwróci true tylko jeśli element faktycznie został dodany (nie było go wcześniej). Poza tym różne kolekcje mogą mieć różną wydajność metod — ale interfejs tego nie gwarantuje.
Jeszcze jeden częsty przypadek: jeśli kolekcja jest używana przez kilka wątków, zmiana kolekcji w jednym wątku i iteracja w innym mogą prowadzić do wyjątków. Do scenariuszy wielowątkowych są specjalne kolekcje (ConcurrentBag<T>, ConcurrentQueue<T> itd. — o tym później).
Jeszcze jedna pułapka: jeśli skopiowałeś kolekcję metodą CopyTo, to potem zmiany w tablicy i w kolekcji będą niezależne od siebie. Tablica — to osobny obszar pamięci.
GO TO FULL VERSION