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

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

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

Como Instalar o Xamarin com C#: Passo a Passo Completo