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
- Pattern matching em C# — a referência oficial da correspondência de padrões; a base desta aula.
- Tuplas — como agrupar e retornar múltiplos valores de forma leve.
- Records — os tipos concisos para carregar dados, com comparação por conteúdo.
- A expressão switch com padrões — o pattern matching aplicado ao
switch. - Novidades do C# (recursos modernos) — o panorama dos recursos recentes da linguagem.
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.