1. Introdução
Se com IEnumerable<T> a gente só podia passear pelo conteúdo já existente da coleção, a interface ICollection<T> é tipo seu passe VIP pro mundo do controle de coleções: dá pra adicionar, remover, checar quantos itens tem, copiar pra array e até monitorar mudanças (tipo quando você tenta controlar o que tem no seu carrinho de compras).
ICollection<T> é implementada por todas as coleções mutáveis do .NET, tipo List<T>, HashSet<T>, Dictionary<TKey, TValue>.ValueCollection e até as menos famosas como Queue<T>, Stack<T> (sim, até filas e pilhas!). É o contrato básico pra todas as classes de coleção que deixam você mexer no conteúdo.
Família de interfaces de coleções
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. O que tem na interface ICollection<T>?
Diferente de IEnumerable<T>, que só serve pra percorrer (foreach), ICollection<T> é tipo um canivete suíço das interfaces: tem método pra tudo!
Olha só o que ela tem de principal:
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 de métodos e propriedades:
- Count — quantidade de elementos na coleção. Tipo aquela função de contar que você curte.
- IsReadOnly — dá pra modificar a coleção? Se true, é só leitura (tipo alguns wrappers).
- Add(T item) — adiciona um item na coleção. Ferramenta essencial!
- Clear() — remove tudo. Limpeza total.
- Contains(T item) — checagem rápida: esse item tá na coleção?
- CopyTo(T[] array, int arrayIndex) — copia os itens pra um array normal, começando de um índice. Útil pra passar dados pra APIs antigas ou métodos "velhos".
- Remove(T item) — remove o item (se ele estiver lá).
3. Desafios reais de programador
Por que é importante entender como essa interface funciona? Na prática, você vai escrever muito código genérico que trabalha com qualquer coleção. Por exemplo, uma função pode receber uma lista, fila ou conjunto — e tanto faz o tipo exato. Se a coleção implementa ICollection<T>, você pode adicionar, remover itens, checar o tamanho e até copiar pra array. Isso é a base pra libs flexíveis e algoritmos genéricos.
Exemplo da vida real: a interface ICollection<T> aparece direto nos parâmetros de métodos e propriedades de APIs que devolvem coleções pra você modificar. Por exemplo, nos ORMs famosos (Entity Framework, Dapper), propriedades do tipo ICollection<T> são usadas pra descrever coleções de objetos relacionados.
Exemplos de uso
Bora criar um método genérico que recebe qualquer coleção e adiciona um item nela. Só precisa implementar ICollection<T>.
// Método genérico pra adicionar item
void AppendItem<T>(ICollection<T> collection, T item)
{
collection.Add(item);
}
Agora dá pra usar esse método tanto com List<T>, quanto com HashSet<T>, ou até com sua própria implementação! Olha só como fica:
var numbers = new List<int> { 1, 2, 3 };
AppendItem(numbers, 42);
var uniqueNames = new HashSet<string> { "Alice", "Bob" };
AppendItem(uniqueNames, "Charlie");
É até engraçado — não importa se é lista ou conjunto, o que interessa é ser ICollection<T>. Não se espante, métodos genéricos assim são super comuns dentro do .NET!
4. Ligação com outras coleções
ICollection<T> não é só um contrato bonito na teoria, mas a base real de praticamente todas as coleções que você vai ver no .NET.
Pra ver na prática como ela é versátil, tenta esse código:
void ShowCollectionInfo<T>(ICollection<T> collection)
{
Console.WriteLine($"Quantidade de itens: {collection.Count}");
Console.WriteLine($"Pode modificar: {!collection.IsReadOnly}");
Console.WriteLine("Itens:");
foreach (var item in collection)
{
Console.WriteLine(item);
}
}
// Pra List<T>
var list = new List<string> { "maçã", "laranja" };
ShowCollectionInfo(list);
// Pra HashSet<T>
var set = new HashSet<string> { "maçã", "laranja" };
ShowCollectionInfo(set);
O resultado vai ser certinho pra qualquer tipo de coleção que implemente essa interface. Testa com fila, pilha, até com array envelopado!
5. Coleções somente leitura
A interface ICollection<T> tem a propriedade IsReadOnly, que diz se dá pra modificar a coleção. Não confunde com a propriedade dos arrays! Se você pegar uma coleção onde IsReadOnly == true, tentar adicionar ou remover item vai dar exceção.
Exemplo:
// Vamos criar um array e envolver em ReadOnlyCollection
var array = new int[] { 1, 2, 3 };
ICollection<int> readOnly = Array.AsReadOnly(array);
// Tenta adicionar um item
try
{
readOnly.Add(4); // Vai lançar NotSupportedException
}
catch (NotSupportedException)
{
Console.WriteLine("Não dá pra adicionar: coleção só leitura!");
}
Isso acontece, por exemplo, quando você recebe uma coleção de fora e não pode mexer — mas pode ler os itens e saber quantos tem.
6. Tabela comparativa dos métodos das coleções populares via ICollection<T>
| Coleção | Add() | Remove() | Contains() | Clear() | CopyTo() | IsReadOnly |
|---|---|---|---|---|---|---|
|
Sim | Sim | Sim | Sim | Sim | Não |
|
Sim* | Sim | Sim | Sim | Sim | Não |
|
Não/Não | Não/Não | Não/Não | Sim/Sim | Sim/Sim | Não |
|
Não | Não | Sim | Não | Sim | Sim |
|
Não | Não | Sim | Não | Sim | Sim/Não* |
Pra HashSet<T> o método Add retorna true se o item foi adicionado, senão false.
7. CopyTo — pra que serve?
O método CopyTo deixa você transferir rapidinho o conteúdo da coleção pra um array normal. Isso é útil se, por exemplo, você precisa devolver dados pra uma função de API antiga que só aceita arrays, ou quer processar tudo de uma vez usando array.
var contactsArray = new Contact[book.Count];
((ICollection<Contact>)book).CopyTo(contactsArray, 0);
// Agora dá pra usar contactsArray do jeito antigo
8. Exemplo
No nosso app de console de estudos, digamos que começamos a criar uma agenda de contatos. Temos a classe Contact, e a agenda pode ser baseada em qualquer coleção, desde que dê pra adicionar, remover e percorrer.
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}");
}
}
}
Agora nossa agenda pode funcionar tanto com List<Contact>, quanto com HashSet<Contact>, ou até — no futuro — com uma coleção própria baseada em banco de dados ou arquivos! Exemplo de 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($"Total de contatos: {book.Count}");
book.PrintAll();
Se quiser evitar duplicados, é só passar um conjunto pra AddressBook em vez de lista:
var book = new AddressBook(new HashSet<Contact>());
(precisa que Contact implemente comparação direitinho, mas isso é papo pra outra aula!)
9. Particularidades dos métodos e erros comuns
Já fica ligado: nem toda coleção que implementa ICollection<T> se comporta igual! Por exemplo, tentar modificar uma coleção quando IsReadOnly == true vai dar exceção. E se você usar HashSet<T>, o método Add só retorna true se o item realmente foi adicionado (não existia antes). Além disso, cada coleção pode ter performance diferente nos métodos — mas isso não é garantido pela interface.
Outro detalhe comum: se a coleção for acessada por várias threads, modificar em uma e percorrer em outra pode dar exceção. Pra cenários multi-thread, existem coleções especiais (ConcurrentBag<T>, ConcurrentQueue<T> e outras — falamos disso depois).
Mais uma pegadinha: se você copiar a coleção com CopyTo, depois as mudanças no array e na coleção ficam independentes. O array é uma área de memória separada.
GO TO FULL VERSION