Capítulo 12: Generics Avançados, Constraints Estritas e Variância

Especialização em Sistemas com C# • C# 12 & .NET 8 • POO Moderna, LINQ, Concorrência e Arquitetura Corporativa


🗺️ Mapa Conceitual da Mecânica de Generics e Variância de Tipos

flowchart TD
    Generics["Programação Genérica no .NET"] --> Constraints["Restrições de Tipo (where T : ...)"]
    Generics --> Variancia["Variância de Tipos em Interfaces / Delegates"]

    subgraph CONSTRAINTS_TYPES["Tipos de Constraints"]
        ClassStruct["where T : class / struct (Referência vs Valor)"]
        Unmanaged["where T : unmanaged (Tipos Blittable Sem Ponteiros GC)"]
        Constructor["where T : new() (Construtor Padrão Sem Parâmetros)"]
        NotNull["where T : notnull (Garante Tipo Não Nulo)"]
    end

    subgraph VARIANCE_TYPES["Covariância e Contravariância"]
        Co["Covariância (out T): Retorno Seguro de Tipos Mais Derivados"]
        Contra["Contravariância (in T): Entrada Segura de Tipos Mais Básicos"]
    end

    Constraints --> CONSTRAINTS_TYPES
    Variancia --> VARIANCE_TYPES

    CONSTRAINTS_TYPES --> JITSpec["JIT Specialization: Código Nativo Otimizado por Tipo de Valor"]

    style Generics fill:#e1f5fe,stroke:#03a9f4,stroke-width:2px
    style CONSTRAINTS_TYPES fill:#ede7f6,stroke:#7e57c2,stroke-width:2px
    style VARIANCE_TYPES fill:#fff3e0,stroke:#ff9800,stroke-width:2px
    style JITSpec fill:#e8f5e9,stroke:#4caf50,stroke-width:2px

🏛️ 1. Como Generics Funcionam no .NET: JIT Specialization vs Code Sharing

Diferente do Java (que utiliza Type Erasure, apagando os tipos genéricos em tempo de compilação e inserindo casts automáticos para Object), o .NET preserva os tipos genéricos reificados em tempo de execução:

  1. Especialização para Tipos de Valor (Value Types): Para cada tipo de valor diferente utilizado (List<int>, List<double>, List<DateTime>), o RyuJIT compila uma cópia especializada de código de máquina nativo. Isso garante que não ocorra nenhum boxing/unboxing, os valores residam diretamente em registradores da CPU e a performance seja idêntica a templates C++.
  2. Compartilhamento de Código para Tipos de Referência (Code Sharing): Para todos os tipos de referência (List<string>, List<Cliente>, List<Pedido>), o JIT compila o código nativo uma única vez, reutilizando as mesmas instruções de máquina para manipular os ponteiros de 64 bits de forma segura.

⚙️ 2. O Arsenal de Generic Constraints (where)

As restrições genéricas instruem o compilador sobre os recursos garantidos que o tipo T deve possuir:

  • where T : struct: Garante que T seja um tipo de valor não anulável.
  • where T : class: Garante que T seja um tipo de referência.
  • where T : notnull: Impede a passagem de tipos anuláveis (string? ou int?).
  • where T : unmanaged: Restrição de baixo nível que exige que T seja uma struct cujos campos também sejam apenas tipos de valor primitivos (sem nenhum ponteiro para o Heap). Habilita ponteiros brutos e operações com sizeof(T) sem código unsafe!
  • where T : new(): Garante a existência de um construtor público sem parâmetros (return new T();).
  • where T : InterfaceBase: Exige conformidade com um contrato específico.

📦 3. Covariância (out) e Contravariância (in)

A variância determina se tipos genéricos com diferentes graus de herança podem ser atribuídos uns aos outros:

3.1 Covariância (out):

Permite usar um tipo mais derivado do que o originalmente especificado. É válida apenas para tipos que atuam como saídas / retornos de métodos:

public interface ILeitor<out T>
{
    T ObterDado(); // T está apenas na SAÍDA
}

// Válido: ILeitor<Gato> pode ser atribuído a ILeitor<Animal>!
ILeitor<Gato> leitorGatos = /* ... */;
ILeitor<Animal> leitorAnimais = leitorGatos;

3.2 Contravariância (in):

Permite usar um tipo mais genérico (base) do que o especificado. É válida apenas para tipos que atuam como entradas / parâmetros de métodos:

public interface IComparador<in T>
{
    bool Comparar(T itemA, T itemB); // T está apenas na ENTRADA
}

// Válido: IComparador<Animal> pode ser usado onde se espera IComparador<Gato>!
IComparador<Animal> compAnimais = /* ... */;
IComparador<Gato> compGatos = compAnimais;

📊 4. Matriz Comparativa: Generics em .NET vs Java vs C++

Dimensão C# / .NET Generics Java Generics C++ Templates
Mecanismo de Execução Reificação no Runtime (CLR) Type Erasure (Apagamento para Object) Expansão de Código em Tempo de Build
Performance com Primitivos Nativa (Zero Boxing) Lenta (obriga boxing para Integer, Double) Nativa (Zero Overhead)
Reflexão em Runtime Tipos refletíveis com precisão Informação de tipo é perdida Inexistente (sem reflexão nativa)
Tempo de Compilação Rápido Rápido Lento (instanciação repetida)

🔬 5. Laboratório de Arquitetura: Unsafe.SizeOf<T>() e Aritmética de Ponteiros Genéricos

Em bibliotecas de processamento de rede e drivers em C#, frequentemente precisamos saber o tamanho exato em bytes de um tipo genérico T. Usar Marshal.SizeOf em runtime é proibitivo por usar reflexão.

A classe System.Runtime.CompilerServices.Unsafe fornece primitivas intrinsics que o compilador JIT substitui por constantes diretas de máquina:

using System.Runtime.CompilerServices;

public static int ObterTamanhoMemoria<T>() where T : unmanaged
{
    // O JIT substitui esta chamada por uma constante imediata no assembly final (ex: mov eax, 4)!
    return Unsafe.SizeOf<T>();
}

Essa técnica viabiliza alocadores de ring buffers genéricos de alta vazão capazes de transacionar centenas de gigabytes de dados por segundo sem gerar coletas de lixo.