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:
// Em vez de uma classe grande:
public record Livro(string Titulo, string Autor, int AnoPublicacao);
Este código gera automaticamente para você:
Um construtor com os parâmetros.
Propriedades (do tipo
init
) que garantem a imutabilidade após a inicialização.Implementações automáticas de igualdade baseada em valor (
Equals()
eGetHashCode()
).
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:
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.
// 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:
// 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:
// 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:
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):
// 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:
// 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>
:
// 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).
// 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
Postar um comentário