Capítulo 12: Generics Avançados, Constraints Estritas e Variância
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:
- 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++. - 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 queTseja um tipo de valor não anulável.where T : class: Garante queTseja um tipo de referência.where T : notnull: Impede a passagem de tipos anuláveis (string?ouint?).where T : unmanaged: Restrição de baixo nível que exige queTseja uma struct cujos campos também sejam apenas tipos de valor primitivos (sem nenhum ponteiro para o Heap). Habilita ponteiros brutos e operações comsizeof(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.