O que são Generics?
Generics em C# constituem um mecanismo de extensão fortemente tipada, empregando um parâmetro de tipo genérico "T" para definir um template de tipo, similar aos templates em C++. Eles possibilitam o encapsulamento e reutilização de código, promovendo a segurança de tipos e reduzindo operações de boxing e unboxing.
- Generics permitem a reutilização de lógica de código através de tipos distintos, como exemplificado por
List<T>eQueue<T>. - Ao substituir o tipo object por parâmetros genéricos, ocorre uma diminuição signifiactiva de boxing, resultando em melhorias de desempenho e segurança.
Tipos genéricos podem ser classes, estruturas, interfaces ou delegados, com a declaração de parâmetros entre colchetes angulares. Por exemplo, class Repositorio<T, S>{} define uma classe genérica com dois parâmetros.
É importante notar que construtores não podem itnroduzir parâmetros genéricos, e diferentes quantidades de parâmetros genéricos permitem sobrecarga, como em interface IMotor<T> e interface IMotor<T, R>.
Restrições de Generics (where)
Sem restrições, o parâmetro genérico "T" pode ser substituído por qualquer tipo. As restrições, declaradas com where T : restrição, limitam o intervalo de tipos permitidos, permitindo o uso de capacidades específicas do tipo restrito.
Principais restrições incluem:
class: restringe a tipos de referência.struct: restringe a tipos de valor não nulos.new(): exige um construtor sem parâmetros.IComparable<T>: permite operações de comparação.
Exemplos de código com restrições:
public T Instanciar<T>() where T : new()
{
return new T();
}
public T ObterMaior<T>(T valor1, T valor2) where T : IComparable<T>
{
return valor1.CompareTo(valor2) > 0 ? valor1 : valor2;
}
public class Calculadora<N, M>
where N : IComparable<N>, new()
where M : struct, INumber<M>
{
public M Valor { get; set; }
public void Adicionar(M quantidade)
{
Valor += quantidade;
}
}
A interface INumber<T>, introduzida no .NET 7, fornece operações matemáticas para tipos numéricos integrados, facilitando a implementação de código genérico para cálculos.
Covariância e Contravariância
Covariância e contravariância permitem conversões implícitas seguras entre tipos genéricos, fundamentadas no Princípio da Substituição de Liskov (LSP). O LSP estabelece que objetos de uma classe derivada podem substituir objetos de sua classe base sem comprometer a correção do programa.
Em C#, a covariância é indicada pela palavra-chave out em parâmetros de tipo de interfaces e delegados genéricos, permitindo que tipos derivados sejam usados onde tipos base são esperados. A contravariância, marcada com in, permite o oposto, sendo útil para parâmetros de entrada.
Exemplo de covariância:
interface IFabricante<out T>
{
T Produzir();
}
class FabricanteString : IFabricante<string>
{
public string Produzir() => "exemplo";
}
// Covariância: conversão implícita de IFabricante<string> para IFabricante<object>
IFabricante<string> fabricanteStr = new FabricanteString();
IFabricante<object> fabricanteObj = fabricanteStr;
Exemplo de contravariância:
interface IProcessador<in T>
{
void Executar(T entrada);
}
class ProcessadorObject : IProcessador<object>
{
public void Executar(object entrada) { }
}
// Contravariância: conversão implícita de IProcessador<object> para IProcessador<string>
IProcessador<object> processadorObj = new ProcessadorObject();
IProcessador<string> processadorStr = processadorObj;
Arrays em C# suportam covariância nativamente para tipos de referência, como em object[] array = new string[5];. Esses conceitos garantem flexibilidade e segurança de tipos em cenários genéricos.