Os genéricos permitem escrever um tipo ou método uma vez e usá-lo com muitos tipos, sem perder a segurança de tipos. Em vez de uma List de object que precisa de casts e aceita qualquer coisa, você tem List<int> e List<Order>, cada uma verificada pelo compilador. O coringa entre os sinais de menor e maior, por convenção T, é um parâmetro de tipo; o tipo que quem chama fornece é o argumento de tipo.
Por que genéricos: o problema que eles resolvem
Antes dos genéricos (C# 1), as coleções guardavam object. Isso compilava, mas os erros só apareciam em tempo de execução, e todo int passava por boxing ao entrar:
var prices = new System.Collections.ArrayList();
prices.Add(9.99m);
prices.Add("12.50"); // compiles: anything is an object
decimal total = 0;
foreach (object p in prices)
total += (decimal)p; // InvalidCastException on the string
Com List<decimal>, o segundo Add é um erro de compilação, o laço não precisa de cast e os decimais são guardados como decimais.
Classes genéricas
Declare os parâmetros de tipo depois do nome da classe e use-os como qualquer outro tipo dentro da classe:
Saída:
Page 2/3: Chen, Dara, more: True
Page 1/1: 90, 72, 85, more: False
Page<string> e Page<int> são dois tipos diferentes produzidos a partir de uma definição. Uma classe pode receber vários parâmetros de tipo (Dictionary<TKey, TValue>); chame-os de T quando há um só e de TSomething quando há vários.
Métodos genéricos e inferência de tipos
Um método pode ter os próprios parâmetros de tipo, seja a classe genérica ou não. Quem chama normalmente omite o argumento de tipo, porque o compilador o infere a partir dos argumentos:
Saída:
2 1
back front
ho ho ho
4.50 EUR, 12.00 EUR, 0.99 EUR
A inferência funciona só a partir dos argumentos, nunca a partir do tipo de retorno. Repeat(0, 3) não precisa de anotação, mas um método como T Create<T>() não tem de onde inferir, então quem chama precisa escrever Create<Customer>(). A maior parte do LINQ são métodos genéricos que dependem dessa inferência: prices.Where(p => p > 1) é Where<decimal> sem que você o escreva.
Restrições: o que T pode ser
Dentro de um método genérico, o compilador só deixa você fazer o que funciona para todo T possível. Sem restrição, isso é pouco mais que o que object suporta. Para chamar CompareTo, criar uma instância ou ler uma propriedade, diga ao compilador o que T precisa ser, com where:
Saída:
17
plum
19.99
42 Zoe
As restrições que você vai usar:
| Restrição | Significado | Permite |
|---|---|---|
where T : class | tipo de referência | atribuir null, usar as |
where T : struct | tipo de valor não anulável | usar T? como Nullable<T> |
where T : new() | construtor público sem parâmetros | chamar new T() (precisa vir por último) |
where T : Entity | deriva de Entity | usar os membros de Entity |
where T : IComparable<T> | implementa a interface | chamar CompareTo |
where T : U | deriva de outro parâmetro de tipo | relacionar dois parâmetros |
where T : unmanaged (C# 7.3) | struct sem referências dentro | ponteiros, stackalloc |
where T : Enum (C# 7.3) | um tipo enum | passar para os métodos de Enum |
where T : notnull (C# 8) | tipo não anulável | anotações de referências anuláveis |
Várias restrições em um parâmetro são separadas por vírgulas; vários parâmetros ganham cada um a sua cláusula where: class Cache<TKey, TValue> where TKey : notnull where TValue : class.
Uma restrição ausente aparece como erro de compilação no ponto em que você usa o membro, por exemplo CS1061 ('T' does not contain a definition for 'CompareTo') ou CS0304 ao chamar new T() sem new().
default(T)
O código genérico às vezes precisa do "valor vazio" de um tipo que ele não conhece. default(T) dá 0 para números, false para bool, null para tipos de referência e tipos anuláveis, e uma struct zerada nos outros casos:
Saída:
31
0
Porto
True
0001-01-01
A segunda linha mostra a fraqueza desse padrão: para tipos de valor, "não encontrado" e "encontrei um 0" parecem iguais. É por isso que os métodos do framework usam a forma TryGet (bool TryGetValue(TKey key, out TValue value)). Desde o C# 7.1 você pode escrever o mais curto return default; quando o tipo fica claro pelo contexto.
Membros static são por tipo construído
Cada tipo construído (Counter<int>, Counter<string>) recebe a sua própria cópia dos campos static da classe:
Saída:
2
1
False
Isso às vezes é útil (um cache por tipo, como um TypeInfo<T>.Name calculado uma vez por T) e às vezes surpreende quando você esperava um contador compartilhado. Coloque o estado compartilhado em uma classe não genérica.
Coleções genéricas
As coleções genéricas cobrem a maioria das necessidades: List<T>, Dictionary<TKey, TValue>, HashSet<T>, Queue<T>, Stack<T> e as interfaces que elas compartilham, IEnumerable<T>, ICollection<T>, IList<T>, IReadOnlyList<T>. Aceite a interface mais restrita de que um método precisa (IEnumerable<T> se ele só percorre) e retorne um tipo concreto ou somente leitura.
Covariância e contravariância
Um Dog é um Animal, mas uma List<Dog> não é uma List<Animal>:
List<Dog> dogs = new List<Dog>();
List<Animal> animals = dogs;
// error CS0029: Cannot implicitly convert type 'List<Dog>' to 'List<Animal>'
Se isso fosse permitido, animals.Add(new Cat()) colocaria um gato na lista de cachorros. Interfaces que só produzem valores são seguras, e são declaradas covariantes com out:
IEnumerable<Animal> readOnly = dogs; // fine: IEnumerable<out T>
IReadOnlyList<Animal> alsoFine = dogs; // fine: IReadOnlyList<out T>
A imagem espelhada, a contravariância, marcada com in, vale para interfaces e delegates que só consomem valores: um Action<Animal> pode ser usado onde se espera um Action<Dog>, porque qualquer coisa que sabe lidar com qualquer animal sabe lidar com um cachorro. IComparer<in T> é contravariante, e Func<in T, out TResult> usa os dois marcadores. A variância só vale para interfaces e delegates, e só para argumentos de tipo de referência: IEnumerable<int> não se converte em IEnumerable<object>.
Erros comuns
- Usar
objectonde deveria estar um parâmetro de tipo. Abre mão das verificações em tempo de compilação e faz boxing dos tipos de valor. - Comparar dois valores
Tcom==. Sem uma restriçãoclassnão compila (CS0019), e com ela compara referências. UseEqualityComparer<T>.Default.Equals(a, b), que funciona para todoTe respeita oEqualsdo próprio tipo. - Retornar
default(T)como "não encontrado". É ambíguo para tipos de valor. Usebool TryX(..., out T value). - Restrições demais. Cada uma reduz quem pode usar o método. Restrinja só o que o corpo realmente precisa.
- Esperar que
List<Derived>se converta emList<Base>. AceiteIEnumerable<Base>ouIReadOnlyList<Base>.
Perguntas frequentes
O que são genéricos em C#?
Os genéricos permitem escrever uma classe, interface ou método uma vez com um tipo coringa, T, que quem chama preenche: List<int>, List<Customer>. O compilador verifica cada uso contra o tipo real, então você tem segurança de tipos sem casts, e os tipos de valor são guardados sem boxing. As coleções de System.Collections.Generic são o exemplo mais conhecido.
Como escrever um método genérico em C#?
Coloque os parâmetros de tipo depois do nome do método: static T Largest<T>(List<T> items) where T : IComparable<T> { ... }. Quem chama normalmente omite o argumento de tipo, porque o compilador o infere a partir dos argumentos: Largest(prices) funciona quando prices é uma List<decimal>. Escreva-o explicitamente, Largest<decimal>(...), quando não há nada de onde inferir.
O que where T : class significa em C#?
É uma restrição: T precisa ser um tipo de referência, então o código genérico pode atribuir null a um T e usar as T. Outras restrições são where T : struct (um tipo de valor não anulável), where T : new() (tem um construtor público sem parâmetros, então o código pode chamar new T()), where T : SomeBaseClass e where T : ISomeInterface, que permite ao código chamar os membros dessa interface em T.
O que é default(T) em C#?
O valor padrão do tipo que T acabar sendo: 0 para números, false para bool, null para tipos de referência e tipos anuláveis, e uma struct zerada para os outros tipos de valor. O código genérico o usa quando precisa de "nenhum valor" sem conhecer o tipo, por exemplo como retorno de uma consulta que falhou. O C# 7.1 adicionou o literal default, mais curto.
Por que não posso atribuir uma List<Dog> a uma List<Animal> em C#?
Porque uma List<Animal> aceita qualquer animal: se a atribuição fosse permitida, o código poderia adicionar um Cat ao que na verdade é uma List<Dog>. O compilador a rejeita (CS0029). Interfaces somente leitura são covariantes, então IEnumerable<Animal> animals = dogs; e IReadOnlyList<Animal> animals = dogs; compilam, já que nada pode ser adicionado por meio delas.