C# Generics
Você já escreveu List<int>, List<string> e Dictionary<string, decimal>. Uma única classe List serve para guardar números, textos, alunos ou qualquer outra coisa, e mesmo assim o compilador sabe exatamente o que está dentro de cada lista. Isso é generics (em português, "genéricos"): escrever uma classe ou um método uma vez, deixando um tipo "em branco", para ser preenchido por quem for usar. Nesta aula você vai criar as suas próprias classes e métodos genéricos.
- Por que generics existem: o problema de guardar tudo como
object. - Criar uma classe genérica:
Caixa<T>. - Os nomes dos parâmetros de tipo (
T,TKey,TValue…) e classes com dois tipos. - Criar métodos genéricos e deixar o compilador deduzir o tipo.
- O
default: o valor "vazio" de qualquer tipo. - Os erros reais:
CS1503,CS0305,CS0411eCS0019(o queTnão sabe fazer).
Todos os programas desta aula rodaram com o .NET 10 na máquina da Ana (usuária ana, computador notebook), na pasta ~/cs, com dotnet run arquivo.cs. Você pode repetir cada um no editor do guia.
A ideia: a caixa com etiqueta
Na oficina da Ana há caixas de plástico iguaizinhas. A fábrica fez um só modelo de caixa. Quando a Ana compra uma, ela cola uma etiqueta: "PARAFUSOS", "PREGOS", "BOTÕES". A partir daí, aquela caixa é de parafusos, e ninguém guarda pregos nela.
Uma classe genérica é o modelo da fábrica, com um espaço para a etiqueta. Esse espaço é o parâmetro de tipo, escrito entre < >, normalmente com a letra T (de type, "tipo"):
class Caixa<T> « o modelo: T é a etiqueta em branco new Caixa<int>(…) « uma caixa com a etiqueta "int" new Caixa<string>(…) « outra caixa, com a etiqueta "string"
Quando você escreve Caixa<int>, o compilador troca o T por int em toda a classe. É como se você tivesse escrito uma classe só para números, e outra só para textos, sem ter escrito nenhuma das duas.
1. A vida sem generics
Antes dos generics (que chegaram no C# 2, em 2005), o jeito de fazer uma caixa "para qualquer coisa" era usar o tipo object, que aceita qualquer valor (veja A Classe object):
var caixa = new CaixaDeQualquerCoisa();
caixa.Guardar("parafusos");
int quantidade = (int)caixa.Abrir();
Console.WriteLine(quantidade);
class CaixaDeQualquerCoisa
{
private object conteudo = "nada";
public void Guardar(object coisa) => conteudo = coisa;
public object Abrir() => conteudo;
}
$ dotnet run semgenerics.cs Unhandled exception. System.InvalidCastException: Unable to cast object of type 'System.String' to type 'System.Int32'. at Program.<Main>$(String[] args) in /home/ana/cs/semgenerics.cs:line 4 $ echo $? 134
(No editor do guia, a mesma exceção sai com uma mensagem mais curta, Specified cast is not valid., "a conversão indicada não é válida". O erro é o mesmo.)
O programa compilou: para o compilador, a caixa guarda object, e um (int) (um cast) de object é permitido. Só na hora de rodar o .NET descobriu que dentro havia um texto, e lançou a InvalidCastException: "não é possível converter um objeto do tipo String no tipo Int32". Com object, você:
- tem de converter (
(int),(string)) toda vez que tira algo da caixa; - só descobre o erro quando o programa roda, talvez na máquina do cliente;
- perde desempenho com números: cada
intguardado numobjecté "embrulhado" num objeto (o boxing).
2. A primeira classe genérica: Caixa<T>
var caixaDeNumero = new Caixa<int>(42);
int numero = caixaDeNumero.Abrir();
Console.WriteLine(numero + 1);
var caixaDeTexto = new Caixa<string>("parafusos");
string texto = caixaDeTexto.Abrir();
Console.WriteLine(texto.ToUpper());
caixaDeTexto.Guardar("pregos");
Console.WriteLine(caixaDeNumero);
Console.WriteLine(caixaDeTexto);
class Caixa<T>
{
private T conteudo;
public Caixa(T conteudo)
{
this.conteudo = conteudo;
}
public void Guardar(T coisa) => conteudo = coisa;
public T Abrir() => conteudo;
public override string ToString() => $"Caixa<{typeof(T).Name}> com {conteudo}";
}
$ dotnet run caixa.cs 43 « o Abrir devolveu um int de verdade: dá para somar PARAFUSOS « e aqui um string: dá para usar ToUpper Caixa<Int32> com 42 Caixa<String> com pregos
- class Caixa<T>
- "Uma caixa de
T". O<T>logo depois do nome declara o parâmetro de tipo. Dentro da classe,Tpode ser usado em qualquer lugar onde caberia um tipo. - private T conteudo;
- Um campo do tipo
T. NumaCaixa<int>, ele é umint; numaCaixa<string>, umstring. - public Caixa(T conteudo)
- O construtor: a caixa já nasce com algo dentro. Repare que o nome do construtor é só
Caixa, sem o<T>. - public void Guardar(T coisa)
- Só aceita
T. NumaCaixa<int>, tentar guardar um texto é erro de compilação (você vai ver logo abaixo). - public T Abrir()
- Devolve um
T. Não precisa de(int)nenhum: o compilador já sabe o tipo. - typeof(T).Name
- O nome do tipo que preencheu o
T, já com o programa rodando:Int32é o nome dointno .NET, eString, o dostring. No C#, o tipo de verdade continua conhecido durante a execução.
O erro agora aparece antes de rodar
Com a caixa genérica, a mesma confusão da seção 1 nem chega a virar programa:
var caixa = new Caixa<int>(42);
caixa.Guardar("parafusos");
$ dotnet run errado.cs /home/ana/cs/errado.cs(2,15): error CS1503: Argumento 1: não é possível converter de "string" para "int" Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
Esse é o grande ganho: o erro que antes era uma exceção na máquina do cliente agora é uma linha vermelha no seu editor, com o lugar exato (linha 2, coluna 15). É a chamada segurança de tipos.
Os nomes dos parâmetros de tipo
| Nome | Costume | Exemplo do .NET |
|---|---|---|
T | Um tipo só, sem papel especial | List<T>, HashSet<T> |
TKey, TValue | "T" + o papel do tipo, quando há mais de um | Dictionary<TKey, TValue> |
TElement, TPriority | Idem | PriorityQueue<TElement, TPriority> |
TResult | O tipo do resultado | Func<T, TResult> (aula Func e Action) |
Os nomes são só convenção (o compilador aceitaria <Banana>), mas siga-a: quem lê o seu código reconhece na hora que TKey é um tipo, e não uma variável.
Dois parâmetros de tipo
var produto = new Par<string, decimal>("café", 18.90m);
var aluno = new Par<string, int>("Ana", 29);
Console.WriteLine($"{produto.Primeiro}: {produto.Segundo:C}");
Console.WriteLine($"{aluno.Primeiro} tem {aluno.Segundo} anos");
Console.WriteLine(produto);
record Par<TPrimeiro, TSegundo>(TPrimeiro Primeiro, TSegundo Segundo);
$ dotnet run par.cs
café: R$ 18,90
Ana tem 29 anos
Par { Primeiro = café, Segundo = 18,90 }
Os parâmetros de tipo vão separados por vírgula, como no Dictionary. Um record também pode ser genérico: aqui, uma linha basta para um par de qualquer coisa com qualquer coisa. (Para pares rápidos, o C# já tem as tuplas; o Par é só para ver a ideia.)
3. Métodos genéricos
Não é só classe que pode ter <T>: um método também. O <T> vem logo depois do nome do método:
int a = 1, b = 2;
Trocar(ref a, ref b);
Console.WriteLine($"a = {a}, b = {b}");
string x = "Ana", y = "Bruno";
Trocar(ref x, ref y);
Console.WriteLine($"x = {x}, y = {y}");
Console.WriteLine(Primeiro(new List<int> { 7, 8, 9 }));
Console.WriteLine(Primeiro<string>(["pão", "leite"]));
void Trocar<T>(ref T primeiro, ref T segundo)
{
T guardado = primeiro;
primeiro = segundo;
segundo = guardado;
}
T Primeiro<T>(List<T> itens) => itens[0];
$ dotnet run metodos.cs a = 2, b = 1 x = Bruno, y = Ana 7 pão
- void Trocar<T>(ref T primeiro, ref T segundo)
- Um método que troca o valor de duas variáveis de qualquer tipo, desde que as duas sejam do mesmo tipo
T. Oref(veja ref, out e in) deixa o método mudar as variáveis de quem chamou. - T guardado = primeiro;
- Uma variável local do tipo
T, para não perder o primeiro valor no meio da troca. - Trocar(ref a, ref b)
- Repare: sem
<int>. O compilador olha os argumentos (aebsãoint) e deduz queTéint. Isso se chama inferência de tipo. - Primeiro<string>(["pão", "leite"])
- Aqui o tipo foi escrito à mão. É preciso quando o compilador não tem como deduzir: os colchetes
[ … ]sozinhos não dizem que coleção é (veja o erroCS0411em Quando dá errado).
Você já usou métodos genéricos sem perceber: o Sum, o Max e o ToList do LINQ, o Array.IndexOf, o Comparer<T>.Create… Por isso eles funcionam com qualquer tipo.
Missão: o método Ultimo só aceita List<int>, por isso a chamada com a lista de nomes não compila. Torne-o genérico (T Ultimo<T>(List<T> itens)) para as duas chamadas funcionarem.
List<int> numeros = [3, 5, 9];
List<string> nomes = ["Ana", "Bruno", "Carla"];
Console.WriteLine($"Último número: {Ultimo(numeros)}");
Console.WriteLine($"Último nome: {Ultimo(nomes)}");
int Ultimo(List<int> itens) => itens[itens.Count - 1];
List<int> numeros = [3, 5, 9];
List<string> nomes = ["Ana", "Bruno", "Carla"];
Console.WriteLine($"Último número: {Ultimo(numeros)}");
Console.WriteLine($"Último nome: {Ultimo(nomes)}");
T Ultimo<T>(List<T> itens) => itens[itens.Count - 1];
4. default: o "vazio" de qualquer tipo
Às vezes um método genérico precisa devolver "nada". Mas o que é "nada" para um T que pode ser int, string ou bool? Para isso existe o default, o valor padrão do tipo:
Console.WriteLine(PrimeiroOuPadrao(new List<int>()));
Console.WriteLine(PrimeiroOuPadrao(new List<string>()) is null);
Console.WriteLine(PrimeiroOuPadrao(new List<decimal> { 2.5m }));
Console.WriteLine(default(int));
Console.WriteLine(default(bool));
T? PrimeiroOuPadrao<T>(List<T> itens)
{
if (itens.Count == 0)
return default;
return itens[0];
}
$ dotnet run padrao.cs 0 « lista de int vazia: default de int é 0 True « lista de string vazia: default de string é null 2,5 0 False « default de bool é false
- return default;
- "Devolva o valor padrão do
T, seja ele qual for":0para números,falseparabool,nullpara classes estring. - T?
- Avisa quem chama que o resultado pode ser o "vazio" (com
string, pode sernull). Sem o?, o compilador reclamaria de devolver um possívelnull. - default(int)
- A forma com o tipo escrito, para pedir o padrão de um tipo específico.
É exatamente o que fazem o GetValueOrDefault do Dictionary e o FirstOrDefault do LINQ.
5. O que o T não sabe fazer
Dentro de um método genérico, o compilador não sabe que tipo vai chegar no T. Por isso, ele só deixa você fazer com T o que qualquer tipo sabe: guardar, devolver, passar adiante, comparar com null, chamar ToString() e Equals(). Somar ou comparar com >, não:
Console.WriteLine(Maior(3, 7));
T Maior<T>(T a, T b)
{
if (a > b)
return a;
return b;
}
$ dotnet run operador.cs /home/ana/cs/operador.cs(5,9): error CS0019: O operador ">" não pode ser aplicado a operandos dos tipos "T" e "T" Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
Faz sentido: e se alguém chamasse Maior com dois Aluno? Qual aluno é "maior"? Para dizer ao compilador "este T só pode ser um tipo que sabe se comparar", você põe uma restrição: where T : IComparable<T>. Esse é o assunto da próxima aula, Restrições (where).
Generics no C# × em outras linguagens
| C# | Java | |
|---|---|---|
Tipos como int | List<int> funciona direto, sem embrulhar cada número | Só List<Integer>: cada número vira objeto |
| O tipo durante a execução | Conhecido: typeof(T).Name dá Int32 | Apagado depois da compilação |
Se você vem do Java, essa é a principal diferença: no C#, List<int> e List<string> são tipos realmente diferentes também enquanto o programa roda (new List<int>().GetType() mostra System.Collections.Generic.List`1[System.Int32]).
Quando dá errado
error CS1503: Argumento 1: não é possível converter de "string" para "int"
O que significa: você passou um valor de outro tipo para um método de uma classe genérica já "etiquetada". Numa Caixa<int>, o Guardar só aceita int.
Como consertar: guarde o tipo certo, ou crie a caixa com a etiqueta certa (new Caixa<string>(…)).
error CS0305: Usar o genérico tipo "Caixa<T>" requer 1 argumentos de tipo
O que significa: você escreveu new Caixa(42), sem a etiqueta. Diferente dos métodos, uma classe genérica não deduz o tipo pelo construtor.
$ dotnet run semtipo.cs /home/ana/cs/semtipo.cs(1,17): error CS0305: Usar o genérico tipo "Caixa<T>" requer 1 argumentos de tipo Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
Como consertar: escreva o tipo: new Caixa<int>(42). Ou declare a variável com o tipo e use new(42): Caixa<int> caixa = new(42);.
error CS0411: Os argumentos de tipo do método "Criar<T>()" não podem ser inferidos com base no uso. Tente especificar explicitamente os argumentos de tipo.
O que significa: o compilador não conseguiu deduzir o T. Com var vazio = Criar();, não há argumento nenhum para olhar, e o var também não diz o tipo.
Como consertar: escreva o tipo na chamada: Criar<int>().
error CS0019: O operador ">" não pode ser aplicado a operandos dos tipos "T" e "T"
O que significa: você usou com T uma operação que nem todo tipo tem (>, +, <…). Em inglês: Operator '>' cannot be applied to operands of type 'T' and 'T'.
Como consertar: restrinja o T (por exemplo, where T : IComparable<T>) e use o método que a restrição garante: a.CompareTo(b) > 0. Veja Restrições (where).
InvalidCastException: Unable to cast object of type 'System.String' to type 'System.Int32'.
O que significa: um código antigo guarda tudo como object e converteu para o tipo errado (seção 1).
Como consertar: troque o object por um parâmetro de tipo T; aí o erro passa a aparecer na compilação.
Referência rápida
| Escrita | O que faz |
|---|---|
class Caixa<T> { … } | Classe genérica com um parâmetro de tipo |
class Par<TA, TB> / record Par<TA, TB>(TA A, TB B); | Dois parâmetros de tipo |
new Caixa<int>(42) | Usa a classe com T = int |
T Primeiro<T>(List<T> itens) | Método genérico |
Primeiro(lista) / Primeiro<int>(lista) | Tipo deduzido / tipo escrito |
default / default(T) | Valor padrão: 0, false, null… |
typeof(T).Name | Nome do tipo que preencheu o T |
where T : … | Restrição (próxima aula) |
Exercícios
1. A etiqueta
Complete para criar uma caixa que guarda decimal:
2. Declarar uma classe genérica
Complete a declaração de uma classe Sacola com um parâmetro de tipo:
3. Qual a principal vantagem de Caixa<T> sobre uma caixa que guarda object?
4. Com T Primeiro<T>(List<T> itens), o que a chamada Primeiro(new List<double> { 1.5 }) faz?
5. O que default vale quando T é string?
Missão final: a sacola genérica
Missão: a classe SacolaDeTexto só guarda textos, e a Ana quer uma sacola de números também. Transforme-a numa classe genérica Sacola<T> (troque string por T onde for o tipo do item) e ajuste as duas criações para new Sacola<string>() e new Sacola<int>().
Frutas: 2 Tem uva? True Números: 3 Tem 8? False
var frutas = new SacolaDeTexto();
frutas.Colocar("uva");
frutas.Colocar("maçã");
Console.WriteLine($"Frutas: {frutas.Quantidade}");
Console.WriteLine($"Tem uva? {frutas.Tem("uva")}");
var numeros = new SacolaDeTexto();
numeros.Colocar(4);
numeros.Colocar(7);
numeros.Colocar(9);
Console.WriteLine($"Números: {numeros.Quantidade}");
Console.WriteLine($"Tem 8? {numeros.Tem(8)}");
class SacolaDeTexto
{
private readonly List<string> itens = [];
public void Colocar(string item) => itens.Add(item);
public bool Tem(string item) => itens.Contains(item);
public int Quantidade => itens.Count;
}
var frutas = new Sacola<string>();
frutas.Colocar("uva");
frutas.Colocar("maçã");
Console.WriteLine($"Frutas: {frutas.Quantidade}");
Console.WriteLine($"Tem uva? {frutas.Tem("uva")}");
var numeros = new Sacola<int>();
numeros.Colocar(4);
numeros.Colocar(7);
numeros.Colocar(9);
Console.WriteLine($"Números: {numeros.Quantidade}");
Console.WriteLine($"Tem 8? {numeros.Tem(8)}");
class Sacola<T>
{
private readonly List<T> itens = [];
public void Colocar(T item) => itens.Add(item);
public bool Tem(T item) => itens.Contains(item);
public int Quantidade => itens.Count;
}
Repare que dentro da Sacola<T> há uma List<T>: um genérico usando outro. O T da sacola "passa" para a lista. É assim que as coleções do .NET são construídas.
Resumo
- Generics deixam um tipo "em branco" (
<T>) numa classe ou método, preenchido por quem usa:Caixa<int>,Caixa<string>. - Comparados com
object, eles dão segurança de tipos (erro na compilação, e nãoInvalidCastException), dispensam conversões e não embrulham números. - Convenção de nomes:
T, ouT+ papel (TKey,TValue). Vários parâmetros vão separados por vírgula. - Métodos genéricos (
T Primeiro<T>(…)) costumam ter o tipo deduzido pelos argumentos; quando não dá, escreva-o (CS0411). Classes genéricas sempre precisam do tipo (CS0305). defaulté o valor padrão de qualquerT:0,false,null.- Sem restrições,
Tnão tem operadores como>e+(CS0019); a próxima aula ensina owhere.