1. O problema que generics resolvem
Antes de generics existirem (C# 1.0), uma coleção guardava object —
qualquer coisa. Isso funcionava, mas exigia converter o tipo toda vez que você lia um item, e o
compilador não impedia misturar tipos incompatíveis na mesma coleção. Generics resolvem isso:
você declara o tipo uma vez, na criação da coleção, e o compilador garante
consistência a partir daí.
ArrayList lista = new ArrayList();
lista.Add(1);
lista.Add("texto"); // compila! mistura tipos
int numero = (int)lista[0]; // cast manual obrigatório
List<int> lista = new List<int>();
lista.Add(1);
// lista.Add("texto"); -- não compila
int numero = lista[0]; // sem cast — já é int
2. List<T> — a coleção mais usada no dia a dia
var clientes = new List<Cliente>();
clientes.Add(new Cliente { Nome = "Ana" });
clientes.Add(new Cliente { Nome = "Bruno" });
clientes.Remove(clientes[0]);
bool existe = clientes.Any(c => c.Nome == "Bruno");
Cliente? encontrado = clientes.FirstOrDefault(c => c.Nome == "Bruno");
int total = clientes.Count;
foreach (var cliente in clientes)
Console.WriteLine(cliente.Nome);
List<T> é o equivalente direto de um Array
do JS, mas tipado — todo item precisa ser do mesmo tipo T. Métodos
como .Any() e .FirstOrDefault() vêm do
LINQ (próximo capítulo), o equivalente C# de .some() e
.find() do JS.
3. Dictionary<TKey, TValue> — pares chave/valor
var estoquePorProduto = new Dictionary<int, int>();
estoquePorProduto[101] = 50; // adiciona ou substitui
estoquePorProduto[102] = 30;
if (estoquePorProduto.TryGetValue(101, out int quantidade))
Console.WriteLine(quantidade); // 50
foreach (var par in estoquePorProduto)
Console.WriteLine($"Produto {par.Key}: {par.Value} unidades");
Acessar uma chave inexistente com estoquePorProduto[999] lança
KeyNotFoundException. Use sempre TryGetValue
quando não tiver certeza de que a chave existe — o mesmo princípio de TryParse
do capítulo 2.
4. Outras coleções úteis
| Coleção | Quando usar |
|---|---|
HashSet<T> | Itens únicos, sem ordem garantida — checagem de existência muito rápida |
Queue<T> | Fila FIFO (primeiro a entrar, primeiro a sair) |
Stack<T> | Pilha LIFO (último a entrar, primeiro a sair) |
IEnumerable<T> | A interface mais genérica — "algo que pode ser percorrido"; usada em assinaturas de método |
5. Escrevendo seu próprio tipo genérico
Generics não são exclusividade das coleções prontas — você pode escrever suas próprias classes e métodos genéricos:
public class Resultado<T>
{
public bool Sucesso { get; }
public T? Dado { get; }
public string? Erro { get; }
private Resultado(bool sucesso, T? dado, string? erro)
=> (Sucesso, Dado, Erro) = (sucesso, dado, erro);
public static Resultado<T> Ok(T dado) => new(true, dado, null);
public static Resultado<T> Falha(string erro) => new(false, default, erro);
}
Resultado<Cliente> ObterCliente(int id)
{
var cliente = BuscarNoBanco(id);
return cliente is not null
? Resultado<Cliente>.Ok(cliente)
: Resultado<Cliente>.Falha("Cliente não encontrado");
}
Resultado<T> funciona com qualquer tipo — Resultado<Cliente>,
Resultado<int>, Resultado<List<Produto>>
— escrito uma única vez. Esse padrão (encapsular sucesso/falha num tipo, em vez de lançar exceção
para todo erro esperado) é comum em código C# moderno e vai reaparecer no módulo de APIs.
6. Onde isso te leva
Coleções são a matéria-prima do próximo capítulo: LINQ, o conjunto de operadores que transforma,
filtra e agrega coleções de forma declarativa — o "primo" mais poderoso dos métodos
.map()/.filter()/.reduce()
que você já usa em JavaScript.