Menu

Genéricos em C#: classes, métodos e restrições genéricas

Como os genéricos funcionam em C#: escrever classes e métodos genéricos com parâmetros de tipo, inferência de tipos, restrições com where (class, struct, new(), classes base, interfaces), default(T), membros static por tipo e covariância com IEnumerable<out T>.

Esta página tem editores executáveis - edite, execute e veja a saída na hora.

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çãoSignificadoPermite
where T : classtipo de referênciaatribuir null, usar as
where T : structtipo de valor não anulávelusar T? como Nullable<T>
where T : new()construtor público sem parâmetroschamar new T() (precisa vir por último)
where T : Entityderiva de Entityusar os membros de Entity
where T : IComparable<T>implementa a interfacechamar CompareTo
where T : Uderiva de outro parâmetro de tiporelacionar dois parâmetros
where T : unmanaged (C# 7.3)struct sem referências dentroponteiros, stackalloc
where T : Enum (C# 7.3)um tipo enumpassar para os métodos de Enum
where T : notnull (C# 8)tipo não anulávelanotaçõ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 object onde 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 T com ==. Sem uma restrição class não compila (CS0019), e com ela compara referências. Use EqualityComparer<T>.Default.Equals(a, b), que funciona para todo T e respeita o Equals do próprio tipo.
  • Retornar default(T) como "não encontrado". É ambíguo para tipos de valor. Use bool 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 em List<Base>. Aceite IEnumerable<Base> ou IReadOnlyList<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.

Coddy programming languages illustration

Aprenda a programar com o Coddy

COMEÇAR