1. Um pouco de história
Lá nos primórdios do C#, só tinha duas alegrias pra quem mexia com coleções: usar arrays ou aquelas coleções "pouco tipadas" tipo ArrayList, onde dava pra jogar qualquer objeto. Parecia liberdade! Mas era só misturar uma string e um número inteiro na mesma coleção pra virar bagunça: não dava pra pegar o elemento de boa sem fazer umas checagens de tipo (que nem sempre funcionavam). Fora os erros bizarros do nada e aquele samba doido de cast (conversão de tipo).
Exemplo de coleção não-genérica (ArrayList):
using System.Collections;
ArrayList coisas = new ArrayList();
coisas.Add(42);
coisas.Add("Olá C#");
int numero = (int)coisas[0]; // OK
string texto = (string)coisas[1]; // OK
int erro = (int)coisas[1]; // BOOM! InvalidCastException (erro em tempo de execução)
Pois é, o compilador nem reclama — o erro só aparece quando roda! É tipo abrir a geladeira e achar uma panela quente lá dentro — surpresa!
Qual é a moral dos generics?
Coleções genéricas (Generics) surgiram pra você poder criar coleções com tipo garantido. Isso traz três vantagens principais:
- Segurança de tipo na hora de compilar. O compilador não deixa você colocar coisa errada na coleção sem querer.
- Praticidade: não precisa ficar convertendo tipo toda hora.
- Performance: não perde tempo com "boxing/unboxing" desnecessário de tipos valor (boxing/unboxing).
2. O que são Generics afinal?
Generics é um jeito mágico de descrever estruturas de dados e métodos pra funcionarem com qualquer tipo, mas ainda assim serem bem tipados.
Imagina uma caixa universal, que serve tanto pra livros quanto pra meias, mas só pra um tipo de cada vez. Se a caixa é "só pra livros", ninguém consegue colocar meias nela. É assim com coleções genéricas: se você cria uma coleção de int, não vai cair um string lá por acidente.
Exemplo de declaração de classe genérica
public class Caixa<T>
{
public T Valor { get; set; }
}
var caixaDeInt = new Caixa<int> { Valor = 42 };
var caixaDeString = new Caixa<string> { Valor = "Olá Generics!" };
T é o "parâmetro de tipo" que diz com o que sua caixa vai trabalhar. O C# vai exigir que você sempre diga certinho qual tipo tá colocando.
Coleções genéricas no .NET
No .NET Framework, praticamente todas as coleções modernas têm versão genérica. Tipo:
- List<T> — lista dinâmica de elementos do tipo T.
- Dictionary<TKey, TValue> — array associativo (dicionário) com chaves e valores.
- Queue<T>, Stack<T> — filas e pilhas.
- e várias outras.
3. Como funciona: por dentro dos Generics
Parâmetros de tipo
Quando você declara uma coleção tipo List<int>, o compilador C# cria uma versão separada (specialization) dessa classe só pro tipo int. Se você declara List<string>, o compilador faz outra versão, mas pra strings, e assim vai.
Pra você, programador, parece mágica:
List<int> numeros = new List<int>();
numeros.Add(1); // Só pode adicionar int
List<string> palavras = new List<string>();
palavras.Add("olá"); // Só pode adicionar string
Se tentar colocar outro tipo, o compilador já reclama:
numeros.Add("erro"); // ERRO na hora de compilar!
Segurança de tipo: Compile-time vs. Run-time
Ou seja, erros desse tipo nem chegam a rodar (run-time). Sua coleção fica blindada — o compilador é tipo um cão de guarda na porta.
Por baixo dos panos
- Pra tipos referência (tipo string, object), nada de casts desnecessários.
- Pra tipos valor (int, double), some o problema de "boxing/unboxing" (boxing/unboxing).
- Usar generics no .NET não gera código a mais — o CLR otimiza tudo na hora do JIT.
4. Como generics melhoram a performance
Vamos reforçar as mágicas que os Generics trazem pro nosso código:
Segurança de tipo (Type Safety):
Isso é o mais importante. Com Generics, o compilador vira seu segurança pessoal, não deixa "estranho" entrar na sua coleção. Você pode confiar que List<Produto> só vai ter objetos Produto, nada de strings ou números perdidos. Isso elimina um monte de erro que antes só aparecia quando rodava e explodia tudo (InvalidCastException). Seu código fica mais confiável e previsível.
Performance:
Como já falamos, pra tipos valor (tipo int, double ou DateTime), Generics deixam tudo mais eficiente na memória e no processador. Pra coleções com milhões de itens ou que mudam muito, isso faz diferença. Em vez de ficar passando laranja de caixa pra saco e de volta, você já coloca direto na caixa certa.
Reaproveitamento de código (Code Reusability):
Generics deixam você escrever um código só que funciona pra vários tipos, sem precisar copiar e colar. Por exemplo, se você precisa de uma função pra trocar dois valores. Sem Generics, teria que fazer SwapInt(ref int a, ref int b), SwapString(ref string a, ref string b), SwapProduto(ref Produto a, ref Produto b) e por aí vai. Com Generics, basta uma função: Swap<T>(ref T a, ref T b). Vale pra coleções também: não precisa de IntList, StringList, ProdutoList – só List<T> resolve. Seu código fica mais enxuto, fácil de manter e escalar.
5. Métodos genéricos e suas próprias classes genéricas
Métodos genéricos
Generics não são só pra coleções! Você pode criar métodos genéricos que funcionam com qualquer tipo. Isso deixa o código bem mais flexível.
//Coloca o tipo-parâmetro T logo depois do nome do método
public static void Trocar<T>(ref T x, ref T y)
{
T temp = x;
x = y;
y = temp;
}
// Usando:
int a = 10, b = 20;
Trocar(ref a, ref b); // Agora a == 20, b == 10
string um = "um", dois = "dois";
Trocar(ref um, ref dois); // Funciona pra string também!
Repara que o compilador C# consegue descobrir o tipo do método pelo tipo das variáveis que você passa. Por isso, no exemplo acima, não precisa passar o tipo pra Trocar.
Suas próprias classes genéricas
Você pode até criar suas próprias "caixas" (classes genéricas). É mais fácil do que parece:
public class Par<TPrimeiro, TSegundo>
{
public TPrimeiro Primeiro { get; set; }
public TSegundo Segundo { get; set; }
}
// Usando:
var par = new Par<int, string> { Primeiro = 42, Segundo = "resposta" };
6. Exemplo de uso de coleções genéricas
Bora voltar pra nossa "lista de tarefas" (To-Do List), que começamos a montar nos exemplos das aulas anteriores. Antes, a gente guardava as tarefas num array ou só mostrava na tela. Agora — vamos guardar dinamicamente numa List<string>.
Exemplo: adicionando tarefas dinamicamente na lista
using System;
using System.Collections.Generic;
class Programa
{
static void Main()
{
List<string> tarefas = new List<string>();
Console.WriteLine("Digite uma tarefa (ou linha vazia pra terminar):");
string entrada;
while (!string.IsNullOrWhiteSpace(entrada = Console.ReadLine()))
{
tarefas.Add(entrada);
Console.WriteLine("Tarefa adicionada! Digite mais (ou linha vazia pra terminar):");
}
Console.WriteLine("\nSua lista de tarefas pra hoje:");
foreach (string tarefa in tarefas)
{
Console.WriteLine("- " + tarefa);
}
}
}
O que tem de bom aqui?
- A coleção cresce sozinha conforme você adiciona tarefas.
- Não tem como colocar nada além de string na lista sem querer.
- Dá pra percorrer e mostrar a lista fácil.
7. Erros comuns de iniciantes, detalhes e dicas
Trocar coleções não-genéricas por genéricas às vezes confunde a galera. Olha só uns casos clássicos que o pessoal novo costuma passar:
- Tentar adicionar elemento do tipo errado (List<int> numeros = new List<int>(); numeros.Add("oi"); // Erro).
- Querer misturar tipos diferentes na mesma coleção — aí tem que usar um tipo base (tipo List<object>) — e depois sempre converter de volta pro tipo certo.
- Esquecer que existe constraints, fazer generic "aberto demais" e tomar erro estranho na hora de compilar.
GO TO FULL VERSION