1. Introduzione
Se con IEnumerable<T> potevamo solo girare tra gli elementi già presenti nella collezione, l'interfaccia ICollection<T> è il tuo pass per il mondo della gestione delle collezioni: puoi aggiungere, rimuovere, controllare il numero di elementi, copiarli in un array e persino monitorare i cambiamenti (sì, quasi come nella vita reale quando cerchi di tenere sotto controllo il contenuto del tuo carrello della spesa).
ICollection<T> è implementata da tutte le collezioni mutabili di .NET, come List<T>, HashSet<T>, Dictionary<TKey, TValue>.ValueCollection e anche da quelle meno popolari come Queue<T>, Stack<T> (sì, anche le code e gli stack!). È il contratto base per tutte le classi di collezioni che permettono di modificare il proprio contenuto.
Famiglia di interfacce delle collezioni
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. Cosa include l'interfaccia ICollection<T>?
A differenza di IEnumerable<T>, che serve solo per l'iterazione (foreach), ICollection<T> è come un coltellino svizzero tra le interfacce: c'è un metodo per ogni esigenza!
Ecco il suo contenuto principale:
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 di metodi e proprietà:
- Count — numero di elementi nella collezione. Tipo la tua funzione di conteggio preferita.
- IsReadOnly — si può modificare la collezione? Se true, la collezione è di sola lettura (ad esempio, per alcuni wrapper).
- Add(T item) — aggiunge un elemento alla collezione. Uno dei tuoi strumenti principali!
- Clear() — elimina tutti gli elementi. Pulizia totale.
- Contains(T item) — controllo veloce: c'è questo elemento nella collezione?
- CopyTo(T[] array, int arrayIndex) — copia gli elementi in un array normale, a partire da un certo indice. Utile per passare dati a vecchie API o metodi "legacy".
- Remove(T item) — rimuove un elemento (se presente).
3. Problemi reali da programmatore
Perché è importante capire come funziona questa interfaccia? Nella pratica scrivi spesso codice generico che lavora con qualsiasi collezione. Per esempio, una funzione può accettare una lista, una coda o un set — e non importa quale tipo concreto sia stato passato. Finché quella collezione implementa ICollection<T>, puoi aggiungere, rimuovere elementi, controllare la dimensione e persino copiare in un array. È la base per librerie flessibili e algoritmi universali.
Esempio dalla vita reale: l'interfaccia ICollection<T> si trova spesso nei parametri dei metodi e nelle proprietà delle API che restituiscono una collezione modificabile. Ad esempio, nei famosi ORM (Entity Framework, Dapper) le proprietà di tipo ICollection<T> vengono usate per descrivere collezioni di oggetti collegati.
Esempi di utilizzo
Proviamo a scrivere un metodo universale che accetta qualsiasi collezione e ci aggiunge un elemento. L'unica condizione — deve implementare ICollection<T>.
// Metodo universale per aggiungere un elemento
void AppendItem<T>(ICollection<T> collection, T item)
{
collection.Add(item);
}
Ora puoi usare questo metodo sia con List<T>, sia con HashSet<T>, e persino con la tua implementazione personalizzata dell'interfaccia! Ecco come si usa:
var numbers = new List<int> { 1, 2, 3 };
AppendItem(numbers, 42);
var uniqueNames = new HashSet<string> { "Alice", "Bob" };
AppendItem(uniqueNames, "Charlie");
Fa quasi ridere — non ci interessa se davanti abbiamo una lista o un set, l'importante è che sia una ICollection<T>. Non sorprenderti, così sono fatti spesso i metodi universali dentro .NET!
4. Relazione con altre collezioni
ICollection<T> non è solo un contratto teoricamente elegante, ma anche la base reale per praticamente tutte le collezioni che incontrerai in .NET.
Per vedere nella pratica quanto sia universale, prova questo codice:
void ShowCollectionInfo<T>(ICollection<T> collection)
{
Console.WriteLine($"Numero di elementi: {collection.Count}");
Console.WriteLine($"Si può modificare: {!collection.IsReadOnly}");
Console.WriteLine("Elementi:");
foreach (var item in collection)
{
Console.WriteLine(item);
}
}
// Per List<T>
var list = new List<string> { "mela", "arancia" };
ShowCollectionInfo(list);
// Per HashSet<T>
var set = new HashSet<string> { "mela", "arancia" };
ShowCollectionInfo(set);
Il risultato sarà corretto per qualsiasi tipo di collezione che implementa questa interfaccia. Prova con una coda, uno stack, persino con un array wrappato!
5. Collezioni di sola lettura
L'interfaccia ICollection<T> contiene la proprietà IsReadOnly, che indica se la collezione può essere modificata. Non confonderla con la proprietà degli array! Se trovi una collezione dove IsReadOnly == true, provare ad aggiungere o rimuovere un elemento lancerà un'eccezione.
Esempio:
// Creiamo un array e lo wrappiamo in una ReadOnlyCollection
var array = new int[] { 1, 2, 3 };
ICollection<int> readOnly = Array.AsReadOnly(array);
// Proviamo ad aggiungere un elemento
try
{
readOnly.Add(4); // Verrà lanciata NotSupportedException
}
catch (NotSupportedException)
{
Console.WriteLine("Non si possono aggiungere elementi: collezione di sola lettura!");
}
Succede, ad esempio, quando ricevi una collezione da una fonte esterna e non puoi modificarla — ma puoi leggere gli elementi e sapere quanti sono.
6. Tabella di confronto dei metodi delle collezioni più popolari secondo ICollection<T>
| Collezione | Add() | Remove() | Contains() | Clear() | CopyTo() | IsReadOnly |
|---|---|---|---|---|---|---|
|
Sì | Sì | Sì | Sì | Sì | No |
|
Sì* | Sì | Sì | Sì | Sì | No |
|
No/No | No/No | No/No | Sì/Sì | Sì/Sì | No |
|
No | No | Sì | No | Sì | Sì |
|
No | No | Sì | No | Sì | Sì/No* |
Per HashSet<T> il metodo Add restituisce true se l'elemento è stato aggiunto, altrimenti false.
7. CopyTo — a cosa può servire
Il metodo CopyTo permette di trasferire velocemente il contenuto della collezione in un array normale. Può essere utile, ad esempio, se devi restituire i dati a una funzione di una vecchia API che accetta solo array, o fare un'elaborazione di massa usando gli array.
var contactsArray = new Contact[book.Count];
((ICollection<Contact>)book).CopyTo(contactsArray, 0);
// Ora puoi usare contactsArray alla vecchia maniera
8. Esempio
Nel nostro progetto console didattico, supponiamo di aver iniziato a sviluppare una rubrica. Abbiamo una classe Contact e la rubrica può essere implementata su qualsiasi collezione, basta che supporti aggiunta, rimozione e iterazione.
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}");
}
}
}
Ora la nostra rubrica può funzionare sia con List<Contact>, sia con HashSet<Contact>, e anche — in futuro — con una collezione personalizzata che implementeremo su database o file! Esempio d'uso:
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($"Totale contatti: {book.Count}");
book.PrintAll();
Se vogliamo evitare duplicati, basta passare un set invece di una lista a AddressBook:
var book = new AddressBook(new HashSet<Contact>());
(serve che Contact implementi correttamente l'uguaglianza, ma di questo parleremo in un'altra lezione!)
9. Caratteristiche dei metodi ed errori tipici
Attenzione subito: non tutte le collezioni che implementano ICollection<T> si comportano allo stesso modo! Ad esempio, provare a modificare una collezione quando IsReadOnly == true lancerà un'eccezione. E se lavori con HashSet<T>, il metodo Add restituisce true solo se l'elemento è stato effettivamente aggiunto (prima non c'era). Inoltre, l'implementazione dei metodi può variare in velocità tra le collezioni — ma l'interfaccia non lo specifica.
Un altro caso comune: se la collezione è usata da più thread, modificarla in un thread e iterarla in un altro può causare eccezioni. Per gli scenari multithread servono collezioni speciali (ConcurrentBag<T>, ConcurrentQueue<T> ecc. — ne parleremo più avanti).
Un'altra trappola: se hai copiato la collezione con CopyTo, poi le modifiche all'array e alla collezione saranno indipendenti. L'array è una zona di memoria separata.
GO TO FULL VERSION