Fundamentos de C# .NET: Generics e Covariância/Contravariância

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> e Queue<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.

Tags: CSharp dotnet generics covariance contravariance

Publicado em 6-23 05:13