Recursos Modernos: Pattern Matching, Tuplas e Records

Recursos Modernos: Pattern Matching, Tuplas e Records

Três recursos do C# moderno que encurtam o código do dia a dia: o pattern matching, que examina a forma de um valor; as tuplas, que agrupam valores sem exigir uma classe; e os records, classes enxutas para carregar dados, com igualdade por conteúdo já implementada.
Linguagem C#

• • 12 min de leitura

Chegamos à última aula sobre a linguagem em si, antes do projeto final e das aplicações do mundo real. Vamos usá-la para conhecer três recursos modernos do C# que tornam o código mais expressivo e conciso ao lidar com dados. Nenhum deles é estritamente indispensável — você poderia programar sem eles —, mas todos são amplamente usados em código C# atual, e conhecê-los deixará seus programas mais elegantes e o preparará para ler o código que encontrará por aí. São eles: o pattern matching (uma forma poderosa de examinar a "forma" e o tipo de um valor), as tuplas (agrupar alguns valores sem criar uma classe inteira), e os records (uma maneira enxuta de criar tipos feitos para carregar dados). Encerraremos, assim, nosso estudo da linguagem com o polimento que o C# moderno oferece.

Pattern matching: examinando a forma de um valor

O pattern matching (correspondência de padrões) é uma forma de examinar um valor e reagir conforme sua forma, tipo ou conteúdo, de maneira concisa. Já vimos uma parte dele: a expressão switch da Aula #12, que mapeava valores a resultados. O pattern matching estende essa ideia. Um uso comum e útil é testar o tipo de um valor e, se corresponder, já obtê-lo naquele tipo. Isso é relevante quando temos um valor que poderia ser de tipos diferentes — por exemplo, guardado numa variável do tipo genérico object, que pode conter qualquer coisa:

object valor = "Olá";   // 'object' pode guardar qualquer tipo

// Testa se 'valor' é uma string E, se for, já o disponibiliza como 'texto':
if (valor is string texto)
{
    Console.WriteLine($"É um texto de {texto.Length} caracteres.");
}

O valor is string texto faz duas coisas de uma vez: verifica se valor é uma string e, se for, cria a variável texto já daquele tipo, pronta para usar. É mais conciso e seguro do que verificar o tipo e depois convertê-lo separadamente. O pattern matching brilha especialmente na expressão switch, onde podemos reagir a diferentes formas de um valor. Veja como classificar um valor conforme seu tipo e conteúdo:

string Descrever(object item) => item switch
{
    int n when n < 0  => "número negativo",   // é int E é negativo
    int n             => $"número {n}",         // é int (qualquer)
    string s          => $"texto de {s.Length} letras",  // é string
    null              => "nada",                // é nulo
    _                 => "algo desconhecido"    // qualquer outra coisa
};

Console.WriteLine(Descrever(-5));      // número negativo
Console.WriteLine(Descrever("casa"));  // texto de 4 letras
Console.WriteLine(Descrever(null));    // nada

Cada linha da expressão switch é um padrão: int n when n < 0 corresponde a um inteiro negativo (o when acrescenta uma condição); string s corresponde a qualquer texto; null corresponde ao nulo; e o _ é o coringa que captura o resto. O pattern matching permite expressar, de forma limpa e legível, uma lógica que, com if aninhados, ficaria bem mais verbosa. É um recurso que você verá com frequência em código moderno, e que torna o exame de valores mais elegante.

Tuplas: agrupar valores sem criar uma classe

Às vezes você quer agrupar dois ou três valores relacionados temporariamente — por exemplo, para devolver múltiplos valores de um método — mas criar uma classe inteira para isso seria exagero. Para esses casos, o C# oferece as tuplas, que agrupam alguns valores numa estrutura leve e sem cerimônia:

// Uma tupla de dois valores nomeados: um texto e um número.
(string Nome, int Idade) pessoa = ("Ana", 30);
Console.WriteLine($"{pessoa.Nome} tem {pessoa.Idade} anos.");   // acesso por nome

O uso mais valioso das tuplas é retornar mais de um valor de um método. Lembre que um método comum devolve um único valor (Aula #17). Com uma tupla, ele pode devolver vários de uma vez, sem que você precise criar uma classe só para agrupá-los:

// Devolve DOIS valores: o menor e o maior de uma lista.
(int Menor, int Maior) MinimoEMaximo(List<int> numeros)
{
    int menor = numeros[0];
    int maior = numeros[0];
    foreach (int n in numeros)
    {
        if (n < menor) menor = n;
        if (n > maior) maior = n;
    }
    return (menor, maior);   // devolve os dois numa tupla
}

var resultado = MinimoEMaximo(new List<int> { 3, 7, 1, 9, 4 });
Console.WriteLine($"Menor: {resultado.Menor}, Maior: {resultado.Maior}");  // Menor: 1, Maior: 9

O método MinimoEMaximo devolve dois valores — o menor e o maior — agrupados numa tupla, acessíveis por nome (resultado.Menor, resultado.Maior). Sem tuplas, você teria de criar uma classe só para carregar esses dois valores, ou usar outros mecanismos mais trabalhosos. A tupla é ideal para agrupamentos pequenos e temporários como esse. Para dados com identidade e vida própria no seu programa (uma Pessoa, um Produto usados em vários lugares), continue preferindo uma classe bem nomeada; a tupla não a substitui, mas resolve com elegância os agrupamentos passageiros, como um retorno múltiplo.

Records: classes enxutas para carregar dados

Ao longo do curso, criamos muitas classes cujo propósito era essencialmente carregar dados: uma Pessoa com nome e idade, um Produto com nome e preço. Para essas classes "de dados", escrever o construtor, as properties, e outros detalhes à mão é repetitivo. O C# moderno oferece uma forma enxuta de criar tais tipos: o record. Um record é declarado numa única linha, e o C# gera automaticamente o construtor, as properties e outros comportamentos úteis:

// Um record: uma classe enxuta para carregar dados. Uma linha faz tudo.
record Pessoa(string Nome, int Idade);

// Uso — o construtor e as properties já existem, gerados automaticamente:
Pessoa p = new Pessoa("Ana", 30);
Console.WriteLine(p.Nome);    // Ana
Console.WriteLine(p.Idade);   // 30

Aquela única linha record Pessoa(string Nome, int Idade); equivale a uma classe com properties Nome e Idade e um construtor que as recebe — tudo gerado para você. O record é ideal quando o propósito de um tipo é simplesmente agrupar e transportar dados, sem lógica complexa. Ele oferece ainda uma conveniência notável que resolve algo que vimos na Aula #33: os records comparam por conteúdo, não por identidade. Lembra que dois objetos diferentes com o mesmo conteúdo eram considerados diferentes por ==? Com records, dois records com os mesmos dados são considerados iguais:

Pessoa a = new Pessoa("Ana", 30);
Pessoa b = new Pessoa("Ana", 30);   // outro record, mesmo conteúdo
Console.WriteLine(a == b);   // True! Records comparam por CONTEÚDO

Isso é frequentemente o que queremos para dados: duas pessoas com o mesmo nome e idade "são a mesma" para fins de comparação. O record oferece esse comportamento de graça, além da concisão. Use records para os seus tipos que existem apenas para carregar dados; use classes comuns quando o tipo tiver comportamento rico ou precisar do controle detalhado que aprendemos nas aulas de orientação a objetos. Ambos têm seu lugar, e reconhecer qual usar é parte do bom gosto que você desenvolverá.

Encerrando o estudo da linguagem

Com esta aula, encerramos nosso estudo da linguagem C# em si. Olhe para o caminho percorrido: você partiu do "o que é programar", passou por variáveis, tipos, decisões, laços, métodos, coleções, e a grande virada dos objetos, com herança, polimorfismo e interfaces; entendeu a memória por dentro, dominou coleções avançadas e o LINQ com seu motor de genéricos e lambdas; e agora sabe tornar seus programas robustos, tratando erros e nulos, e escrevê-los com a elegância dos recursos modernos. É um repertório completo — o de um programador C# de verdade. As peças, que começaram soltas, formam hoje um todo coeso e poderoso nas suas mãos.

O estudo da linguagem se encerra com três recursos que encurtam consideravelmente o código do dia a dia. O pattern matching examina a forma de um valor e reage conforme ela, dispensando cadeias de verificações e conversões. As tuplas agrupam valores sem exigir uma classe, o que resolve bem o caso do método que precisa devolver mais de um resultado. Os records entregam classes enxutas para carregar dados, com igualdade por conteúdo já pronta.

Nenhum deles inaugura um conceito novo: todos são formas mais diretas de expressar coisas que já eram possíveis. É esse o sinal de uma linguagem madura, e reconhecê-los torna legível o C# moderno que se encontra em código profissional.

Fontes e leituras recomendadas

Exercícios

Exercício 1

Escreva um método string Classificar(object valor) que use uma expressão switch com pattern matching para retornar: "inteiro par" ou "inteiro ímpar" para um int (usando when), "texto" para uma string, "nulo" para null, e "desconhecido` para o resto. Teste com vários tipos de valor.

Ver resposta

✓ Resposta: Exemplo:

string Classificar(object valor) => valor switch
{
    int n when n % 2 == 0 => "inteiro par",
    int n                 => "inteiro ímpar",
    string s              => "texto",
    null                  => "nulo",
    _                     => "desconhecido"
};
Console.WriteLine(Classificar(4));      // inteiro par
Console.WriteLine(Classificar(7));      // inteiro ímpar
Console.WriteLine(Classificar("oi"));   // texto
Console.WriteLine(Classificar(null));   // nulo

Cada padrão corresponde a uma forma diferente do valor, com o when refinando o caso dos inteiros.

Exercício 2

Escreva um método que receba uma List<int> e devolva, numa tupla, a soma e a média dos elementos. Chame-o e exiba os dois valores acessando-os por nome. Explique a vantagem de usar uma tupla em vez de criar uma classe para isso.

Ver resposta

✓ Resposta: Exemplo:

(int Soma, double Media) Estatisticas(List<int> numeros)
{
    int soma = 0;
    foreach (int n in numeros) soma += n;
    return (soma, (double)soma / numeros.Count);
}
var r = Estatisticas(new List<int> { 2, 4, 6 });
Console.WriteLine($"Soma: {r.Soma}, Média: {r.Media}");   // Soma: 12, Média: 4

A vantagem de usar uma tupla é evitar criar uma classe inteira só para agrupar dois valores que serão usados temporariamente: a tupla permite devolver a soma e a média juntas, acessíveis por nome, sem a cerimônia de definir um tipo dedicado. Para um agrupamento passageiro como o retorno de um cálculo, a tupla é mais leve e direta.

Exercício 3

Reescreva uma das classes "de dados" que você criou em aulas anteriores (por exemplo, Produto com Nome e Preco) como um record de uma linha. Crie dois records com os mesmos dados e compare-os com ==, confirmando que dá True. Explique por que isso difere do comportamento de uma classe comum (Aula #33).

Ver resposta

✓ Resposta: Exemplo:

record Produto(string Nome, decimal Preco);
Produto a = new Produto("Caderno", 12.5m);
Produto b = new Produto("Caderno", 12.5m);
Console.WriteLine(a == b);   // True

Dá True porque records comparam por conteúdo: dois records com os mesmos dados (mesmo nome e preço) são considerados iguais. Isso difere de uma classe comum, cujo == compara por identidade (se é o mesmo objeto na memória), como vimos na Aula #33 — duas instâncias de classe com os mesmos dados dariam False, por serem objetos distintos. O record oferece a comparação por conteúdo automaticamente, o que é frequentemente o desejado para tipos que carregam dados.

Exercício 4

Explique, com suas palavras, o que faz a expressão valor is string texto no pattern matching. Que duas coisas ela realiza de uma vez, e por que isso é mais conciso do que verificar o tipo e converter separadamente?

Ver resposta

✓ Resposta: A expressão valor is string texto verifica se valor é do tipo string e, se for, cria a variável texto já contendo aquele valor como uma string pronta para uso. As duas coisas que ela realiza de uma vez são: o teste de tipo (é uma string?) e a disponibilização do valor no tipo certo (a variável texto, já como string). Isso é mais conciso do que verificar o tipo e converter separadamente porque, sem o pattern matching, seria preciso primeiro testar o tipo e depois, num passo adicional, converter o valor para aquele tipo antes de usá-lo — duas operações separadas, com mais código e mais chance de inconsistência. O pattern matching une teste e obtenção num só passo, seguro e legível.

Exercício 5

Sem código: explique quando você usaria uma tupla, quando usaria um record, e quando usaria uma classe comum. Dê um exemplo de dado apropriado para cada um, justificando a escolha.

Ver resposta

✓ Resposta: Usaria uma tupla para agrupar alguns valores de forma temporária e sem identidade própria — por exemplo, o retorno de um método que devolve a soma e a média de uma lista (um agrupamento passageiro que não merece um tipo dedicado). Usaria um record para um tipo cujo propósito é carregar dados, usado em vários lugares mas sem comportamento complexo — por exemplo, um Endereco(Rua, Numero, Cidade) que se beneficia da concisão e da comparação por conteúdo. Usaria uma classe comum quando o tipo tiver comportamento rico ou precisar do controle detalhado da orientação a objetos — por exemplo, uma ContaBancaria com métodos Depositar e Sacar, encapsulamento do saldo e validações, onde a lógica e a proteção do estado importam mais do que a mera concisão. A regra: agrupamento temporário → tupla; dados com nome e vida própria, sem muita lógica → record; comportamento rico e estado protegido → classe.

Comentários

Mais em Linguagem C#

Construtores: Objetos que Nascem Completos
Construtores: Objetos que Nascem Completos

O construtor, método especial que roda na criação do objeto e permite exigir…

A Instrução switch: Escolhendo Entre Muitas Opções
A Instrução switch: Escolhendo Entre Muitas Opções

Quando comparar um mesmo valor contra muitas opções, encadear condições fica…

Encerramento: Você é um Programador
Encerramento: Você é um Programador

O encerramento do núcleo do curso: uma retrospectiva da transformação vivida…