C# Moderno: Records, Tuples e Pattern Matching para Modelagem de Dados Imutável

Se você ainda está modelando dados em C# com classes tradicionais e verificações de tipo verbose, está perdendo uma grande oportunidade de escrever um código mais limpo, seguro e funcional. As versões mais recentes do C# (a partir do C# 9) trouxeram recursos que modernizam radicalmente o tratamento de dados.

Vamos mergulhar em três ferramentas poderosas – Records, Tuples e Pattern Matching – que incentivam a imutabilidade e simplificam a lógica condicional.

1. A Importância da Imutabilidade

Antes de tudo, por que se preocupar com a imutabilidade? Um objeto imutável é aquele que não pode ter seu estado alterado após a criação.

Na prática, isso significa:

  • Segurança de Thread: Em ambientes multi-thread, você não precisa se preocupar com um thread alterando o objeto enquanto outro o lê.

  • Previsibilidade: Seu código fica mais fácil de depurar e entender, pois o valor de um objeto é garantido.

  • Códigos Limpos: Remove a necessidade de validadores e getters/setters complexos para controlar alterações.

2. Diga Olá aos Records para Modelos de Dados Imutáveis

Historicamente, criar uma classe de dados imutável em C# era verboso. Você precisava de: um construtor, propriedades readonly, sobrescrever Equals() e GetHashCode(), e, muitas vezes, implementar um método de cópia (Cloning).

Os records (introduzidos no C# 9) resolvem isso de forma elegante.

Sintaxe Concisa (Primary Constructor)

Com a sintaxe de construtor primário, você define as propriedades de forma concisa:

C#
// Em vez de uma classe grande:
public record Livro(string Titulo, string Autor, int AnoPublicacao);

Este código gera automaticamente para você:

  1. Um construtor com os parâmetros.

  2. Propriedades (do tipo init) que garantem a imutabilidade após a inicialização.

  3. Implementações automáticas de igualdade baseada em valor (Equals() e GetHashCode()).

A Mágica do with (Mutação Não Destrutiva)

Como um record é imutável, como você "muda" um valor? Você usa a expressão with para criar uma nova instância com as propriedades desejadas modificadas:

C#
var meuLivro = new Livro("A Bússola de Ouro", "Philip Pullman", 1995);

// Cria uma *nova* instância com o ano atualizado.
// O objeto 'meuLivro' original permanece inalterado.
var livroReeditado = meuLivro with { AnoPublicacao = 2024 };

Console.WriteLine(livroReeditado); 
// Output: Livro { Titulo = A Bússola de Ouro, Autor = Philip Pullman, AnoPublicacao = 2024 }

3. Tuples (Tuplas): Retornos Leves e Rápidos

As Tuplas não são novidade, mas seu uso moderno permite retornar múltiplos valores de um método de forma limpa, sem a necessidade de criar uma nova classe ou struct dedicada apenas para isso.

C#
// Retornando Tuplas com Nomes de Campo explícitos
public (bool Sucesso, string Mensagem, int Id) ProcessarPedido(decimal valor)
{
    if (valor <= 0)
    {
        return (false, "Valor inválido.", 0);
    }
    // Lógica de processamento
    return (true, "Pedido processado com sucesso!", 42);
}

// Consumindo o método
var resultado = ProcessarPedido(100.00m);
if (resultado.Sucesso)
{
    Console.WriteLine($"ID do Pedido: {resultado.Id}");
}

As tuplas são ideais para uso local ou dentro de uma camada de código, atuando como um "pacote de dados" temporário, leve e auto-descritivo.

4. Pattern Matching: Simplificando a Lógica Condicional

A cereja do bolo é o Pattern Matching, especialmente quando combinado com records e tuplas. Ele transforma sequências complexas de if/else ou switch em expressões concisas e declarativas.

O Poder do switch Expression

Use a switch expression para avaliar uma expressão e retornar um valor com base em vários padrões (tipos, propriedades, valores, etc.).

No exemplo abaixo, combinamos Pattern Matching com a desconstrução de um record para definir o frete:

C#
// Exemplo de Record:
public record Endereco(string Estado, int PesoPacote);

public decimal CalcularFrete(Endereco endereco) => endereco switch
{
    // 1. Pattern de Propriedade: Frete Grátis para SP
    { Estado: "SP" } => 0.00m,
    
    // 2. Pattern Relacional (combinação de condições)
    { PesoPacote: > 10 } => 25.00m, // Mais de 10kg
    
    // 3. Pattern de Descarte: Catch-all (o default)
    _ => 15.00m
};

Benefícios:

  • Leitura Melhorada: A lógica é declarada, não imperativa.

  • Redução de Código: Elimina castings e várias verificações de if/else if.

  • Segurança: O compilador pode alertar se você não cobrir todos os casos (exhaustive checking).

Ao incorporar Records, Tuples e Pattern Matching no seu fluxo de trabalho, você não está apenas usando recursos mais novos do C#, mas adotando um estilo de programação que prioriza a imutabilidade, a clareza e a segurança de Thread.

Abrace o C# moderno e observe a qualidade e a concisão do seu código melhorarem significativamente.


O Próximo Nível do Pattern Matching: Padrões de Lista (List Patterns)

O Pattern Matching evoluiu para permitir que você inspecione não apenas a forma de um objeto ou suas propriedades, mas também a forma de uma sequência de elementos (como arrays ou Lists). Introduzidos no C# 11, os Padrões de Lista (List Patterns) levam o fluxo de decisão declarativo a um novo patamar.

Com a sintaxe de colchetes [], você pode desconstruir coleções e tomar decisões com base nos elementos em posições específicas.

Desconstrução e Verificação de Sequência

Imagine que você precisa verificar se uma sequência de comandos segue um padrão específico:

C#
// Exemplo: Analisando uma sequência de comandos
string[] comandos = ["ADD", "ITEM_1", "QTD_5", "FINALIZE"];

bool comandoValido = comandos switch
{
    // Verifica se a sequência tem 4 elementos E começa com "ADD"
    ["ADD", _, _, "FINALIZE"] => true, 
    
    // Verifica se a sequência tem 3 elementos, começando com "CLEAR"
    ["CLEAR", "ALL", _] => true, 
    
    // Qualquer outra coisa é inválida
    _ => false
};

Console.WriteLine($"Comando válido: {comandoValido}"); // Output: True

O Pattern de Intervalo (Range Pattern) ..

O operador .. (padrão de intervalo ou splice) é incrivelmente poderoso, permitindo que você capture ou ignore um número zero ou mais de elementos no meio da lista.

Se você quer garantir que o primeiro item seja "Header" e o último seja "Footer", independentemente do que está no meio:

C#
string[] log = ["Header", "Entry 1", "Entry 2", "Entry 3", "Footer"];

if (log is ["Header", .., "Footer"])
{
    Console.WriteLine("O arquivo de log está bem formatado!");
}

Você também pode capturar os elementos intermediários em uma nova variável para processamento posterior: ["Header", ..var corpo, "Footer"].


C#: Construtores Primários Universais e Expressões de Coleção

Recentemente, o C# 12 trouxe a simplificação do código para classes e coleções, tornando o desenvolvimento ainda mais conciso e performático.

Construtores Primários para Classes e Structs (Não Apenas Records)

O C# 12 estendeu a conveniência dos construtores primários para todas as classes e structs (não apenas records).

Isso elimina o boilerplate de declarar o parâmetro do construtor, atribuí-lo a um campo e, depois, inicializar uma propriedade.

Cenário de Injeção de Dependência (Dependency Injection)

Em um Controller ou Service, o parâmetro do construtor primário fica acessível a todos os membros da classe, ideal para Dependency Injection (DI):

C#
// Antes do C# 12, para Classes:
// public class MeuServico { private readonly ILog _log; public MeuServico(ILog log) { _log = log; } ... }

// Com Primary Constructor (C# 12):
public class MeuServico(ILog log)
{
    // 'log' está em escopo e pode ser usado aqui:
    public void ExecutarAcao()
    {
        log.Info("Ação iniciada.");
        // ...
    }
}

Expressões de Coleção Concisas (Collection Expressions)

O C# 12 também introduziu uma sintaxe unificada e concisa para inicializar coleções, usando a mesma sintaxe de colchetes [] dos List Patterns.

Isso funciona para arrays, List<T>, Span<T> e muitas outras coleções:

C#
// Antes:
List<int> listaAntiga = new List<int> { 1, 2, 3 };

// Com Collection Expression (C# 12):
int[] numeros = [1, 2, 3];
List<string> nomes = ["Alice", "Bob"];

// Usando o operador de propagação (Spread Operator: ..) para combinar coleções:
int[] outrosNumeros = [4, 5];
int[] combinados = [..numeros, 0, ..outrosNumeros]; // Resultado: [1, 2, 3, 0, 4, 5]

Essa sintaxe não só reduz a verbosidade, mas também permite que o compilador gere código mais eficiente, otimizando a capacidade da coleção desde o início.


Considerações de Performance: Evitando Alocações na Heap

Para desenvolvedores que trabalham com código de alta performance, a imutabilidade deve ser combinada com o gerenciamento eficiente de memória.

Usando stackalloc com Span<T>

Quando você precisa de um buffer temporário, mas quer evitar a sobrecarga do Garbage Collector (GC) e alocações na Heap, use a palavra-chave stackalloc em combinação com Span<T>:

C#
// Cria um buffer de 10 inteiros *na Stack*
Span<int> buffer = stackalloc int[10]; 

// O buffer existe apenas dentro do escopo do método,
// garantindo liberação imediata e zero alocação na Heap.
for (int i = 0; i < buffer.Length; i++)
{
    buffer[i] = i * 2;
}

Essa é uma técnica essencial para cenários como parsing rápido, manipulação de buffers e código de rede, onde cada milissegundo e cada alocação importam.

ref readonly Parameters (C# 12)

Para estruturas de dados maiores (structs grandes) que são imutáveis, o C# 12 introduziu ref readonly parameters. Isso permite passar o dado por referência (evitando a cópia completa do struct), mas garantindo que o método chamado não possa modificá-lo (mantendo a imutabilidade).

C#
// Struct grande de 512 bytes
public readonly struct BigData(byte[] data); 

// Passa o dado por referência para performance, mas é readonly.
public void ProcessarRapido(ref readonly BigData dado)
{
    // dado.data = new byte[1]; // ERRO: Não pode modificar!
    // Apenas pode ler os dados.
}

Conclusão

O C# moderno, a partir das versões 9, 11 e 12, se consolidou como uma linguagem que equilibra o poder da programação orientada a objetos com as vantagens da programação funcional (como a imutabilidade e a lógica declarativa). Recursos como Records, Pattern Matching em Listas e Expressões de Coleção não são apenas sintaxe mais curta; eles representam uma mudança fundamental em direção a um código mais seguro, previsível e eficiente.

Ao dominar essas ferramentas, você não apenas moderniza seu código, mas também o prepara para os desafios de desempenho e escalabilidade do desenvolvimento de software contemporâneo.

Com a introdução dos Construtores Primários Universais, você ainda vê valor nas classes tradicionais com inicialização explícita? Compartilhe sua opinião!

Comentários

Postagens mais visitadas deste blog

Gerando Relatórios em PDF com Python (ReportLab e FPDF)

Manipulação de Arquivos no C#: Como Ler, Escrever e Trabalhar com Arquivos de Forma Simples

Laços de Repetição em Python: Conceitos e Exemplos Práticos