C# Pattern Matching

Pattern matching (em português, "casamento de padrões") é perguntar "este valor tem esta forma?" e, na mesma tacada, pegar as partes dele para usar. "É um texto? Então me dê o texto." "É uma pessoa com menos de 18 anos?" "É uma lista que começa com 1 e 2?" Desde o C# 7, a cada versão a linguagem ganhou um tipo novo de padrão, e hoje eles aparecem em todo código moderno, no is e no switch.

Você vai aprender
  • O is com padrão de tipo C# 7: testar o tipo e já ganhar uma variável.
  • Onde essa variável pode ser usada (erros CS0165 e CS0136), is null e is not C# 9.
  • Padrões relacionais e lógicos C# 9: >= 7, and, or, not.
  • O switch com padrões e guardas when.
  • Padrões de propriedade ({ Idade: < 18 }), posicionais ((0, 0)), de tupla e de lista C# 11 ([1, 2, ..]).
  • Por que o switch precisa cobrir todos os casos (aviso CS8509 e a exceção SwitchExpressionException) e o erro CS8510.

Esta aula usa o que você viu em polimorfismo (uma variável object guarda um string, um int…), conversão de tipos, expressões switch e records. Os exemplos rodaram com dotnet run no .NET 10 da máquina da Ana, na pasta ~/cs.


A ideia antes do código

Imagine a Ana separando as encomendas da loja. Chega uma caixa fechada. Ela pergunta: "isto é uma caixa de frutas?". Se for, ela abre e pega as frutas. Se for de livros, faz outra coisa. A pergunta e o "abrir" andam juntos: ninguém pergunta "é de frutas?", ouve "sim" e depois confere de novo antes de abrir.

No C# antigo era isso que acontecia: o programa perguntava o tipo com is ("é um…?") e, mesmo depois do "sim", precisava converter o valor com um cast. Um padrão junta as duas coisas: "se for um string, chame-o de texto". E o padrão pode perguntar muito mais que o tipo: o valor, as propriedades, os itens de uma lista.


1. O jeito antigo e o padrão de tipo C# 7

Uma variável do tipo object aceita qualquer valor. Mas, por ser genérica, não deixa você chamar métodos de string, como ToUpper() (deixar em maiúsculas). Era preciso testar e converter:

object coisa = "banana";

// Jeito antigo: testar, depois converter (cast)
if (coisa is string)
{
    string texto = (string)coisa;
    Console.WriteLine("Antigo: " + texto.ToUpper());
}

// Jeito novo (C# 7): testar e já dar um nome
if (coisa is string texto2)
{
    Console.WriteLine("Novo: " + texto2.ToUpper());
}
$ dotnet run tipo.cs
Antigo: BANANA
Novo: BANANA
coisa
O valor que você quer examinar.
is
"É…?". A expressão inteira dá true ou false.
string
O tipo que você está testando.
texto2
A novidade: o nome de uma variável de padrão. Se o teste der true, ela já nasce do tipo string, com o mesmo valor. Sem cast.

O conjunto string texto2 é o padrão de tipo (em inglês, declaration pattern): um tipo e um nome. Por que o nome texto2, e não texto de novo? Porque a variável de padrão de um if vale no bloco em volta, e já existe um texto lá dentro; o erro que isso dá está em "Quando dá errado". Com vários else if, cada tipo é tratado do seu jeito:

Descrever("maçã");
Descrever(21);
Descrever(7.5);
Descrever(true);
Descrever(null);

void Descrever(object? item)
{
    if (item is string s)
    {
        Console.WriteLine($"Texto com {s.Length} letras: {s}");
    }
    else if (item is int n)
    {
        Console.WriteLine($"Número inteiro; o dobro é {n * 2}");
    }
    else if (item is double d)
    {
        Console.WriteLine($"Número decimal; a metade é {d / 2}");
    }
    else if (item is null)
    {
        Console.WriteLine("Nada (null)");
    }
    else
    {
        Console.WriteLine($"Não sei descrever: {item}");
    }
}
$ dotnet run descrever.cs
Texto com 4 letras: maçã
Número inteiro; o dobro é 42
Número decimal; a metade é 3,75
Não sei descrever: True
Nada (null)

Cada chamada caiu num ramo. O 21 casou com int n, e dentro do ramo n é um número de verdade: dá para fazer n * 2. O true é um bool, não casou com nenhum padrão e foi para o else. E o null? Ele nunca casa com um padrão de tipo (null is string s dá false), por isso existe o is null.

Onde a variável de padrão pode ser usada

A variável s só pode ser lida onde o compilador tem certeza de que o teste deu certo:

  • Dentro do if que testou: pode.
  • Depois de um && (e): pode, porque o lado direito só roda se o esquerdo deu true. o is string s && s.Length > 3 funciona.
  • Depois de um || (ou): não, porque o lado direito roda justamente quando o teste deu false.
  • Com is not e um return: pode depois do if, porque só chega lá quem passou no teste.
BoasVindas("Ana");
BoasVindas(10);

object[] itens = ["Ana", 42, "kiwi", 3.5];
foreach (var o in itens)
{
    if (o is string s && s.Length > 3)
    {
        Console.WriteLine($"Palavra longa: {s}");
    }
}

void BoasVindas(object nome)
{
    if (nome is not string s)
    {
        Console.WriteLine("Isso não é um nome.");
        return;
    }
    Console.WriteLine($"Olá, {s}!");
}
$ dotnet run escopo.cs
Olá, Ana!
Isso não é um nome.
Palavra longa: kiwi

"Ana" é texto, mas tem só 3 letras, então o && deu false; 42 e 3.5 nem são textos. Só "kiwi" passou. Na BoasVindas, o is not C# 9 ("não é") inverte o teste: quem não é texto sai pelo return, e quem continua é texto, com s garantido.

is null e is not null

Para testar se algo é nulo, o código moderno escreve x is null e x is not null em vez de x == null e x != null. O resultado é o mesmo na maioria dos casos, mas o is não pode ser enganado por uma classe que redefiniu o == (veja Sobrecarga de Operadores) e se lê como português: "nome não é nulo".

Missão: o array itens mistura textos e números. Use is com padrão de tipo (o is int n) para somar só os inteiros e mostrar Soma: 49.

object[] itens = ["Ana", 42, "kiwi", 7, 2.5];
int soma = 0;
foreach (var o in itens)
{
    // teste aqui se o é um int e some
}
Console.WriteLine($"Soma: {soma}");
object[] itens = ["Ana", 42, "kiwi", 7, 2.5];
int soma = 0;
foreach (var o in itens)
{
    if (o is int n)
    {
        soma += n;
    }
}
Console.WriteLine($"Soma: {soma}");

2. Padrões de constante, relacionais e lógicos C# 9

Um padrão não precisa ser um tipo. Pode ser um valor fixo (padrão de constante: is 'a', is null), uma comparação (padrão relacional: is >= 7) e combinações com as palavras and (e), or (ou) e not (não):

char c = 'q';
Console.WriteLine(c is >= 'a' and <= 'z');
Console.WriteLine(c is 'a' or 'e' or 'i' or 'o' or 'u');
string? nome = null;
Console.WriteLine(nome is null);
Console.WriteLine(nome is not null);
int dia = 6;
Console.WriteLine(dia is 6 or 7 ? "fim de semana" : "dia útil");
$ dotnet run relacionais.cs
True
False
True
False
fim de semana
LinhaLê-seSem padrão seria
c is >= 'a' and <= 'z'"c está entre a e z" (é letra minúscula)c >= 'a' && c <= 'z'
c is 'a' or 'e' or 'i' or 'o' or 'u'"c é uma vogal"c == 'a' || c == 'e' || …
nome is null"nome é nulo"nome == null
dia is 6 or 7"dia é 6 ou 7"dia == 6 || dia == 7

A vantagem: o valor testado (c, dia) aparece uma vez só. Em dia is 6 or 7 é impossível cair no erro clássico de escrever dia == 6 || 7. Repare que and, or e not só valem dentro de padrões; fora deles, continue com &&, || e !.

No switch

Os padrões relacionais brilham numa expressão switch. Os braços são testados de cima para baixo, e o primeiro que casar ganha:

Console.WriteLine(Classificar(9));
Console.WriteLine(Classificar(6.5));
Console.WriteLine(Classificar(3));
Console.WriteLine(Classificar(11));

string Classificar(double nota) => nota switch
{
    < 0 or > 10 => "nota inválida",
    >= 7 => "aprovada",
    >= 5 => "recuperação",
    _ => "reprovada",
};
$ dotnet run notas.cs
aprovada
recuperação
reprovada
nota inválida

O 11 casou logo no primeiro braço (> 10). O 6.5 não casou com >= 7, mas casou com >= 5. O _ (sublinhado) é o descarte: casa com qualquer coisa, como o default do switch antigo.

Missão: complete o switch com padrões relacionais: abaixo de 0 é congelando, abaixo de 15 é frio, abaixo de 25 é agradável e o resto é quente.

double[] temperaturas = [8.5, 15, 22.4, 31, -3];
foreach (var t in temperaturas)
{
    string clima = t switch
    {
        // escreva os braços aqui
        _ => "?",
    };
    Console.WriteLine($"{t} °C: {clima}");
}
double[] temperaturas = [8.5, 15, 22.4, 31, -3];
foreach (var t in temperaturas)
{
    string clima = t switch
    {
        < 0 => "congelando",
        < 15 => "frio",
        < 25 => "agradável",
        _ => "quente",
    };
    Console.WriteLine($"{t} °C: {clima}");
}

3. Padrões de tipo no switch, com when

Uma escada de if … else if testando tipos fica comprida. No switch, cada braço pode ser um padrão de tipo. E quando o padrão sozinho não basta, a guarda when ("quando") acrescenta uma condição qualquer:

Console.WriteLine(Descrever(10));
Console.WriteLine(Descrever(2.5));
Console.WriteLine(Descrever("pera"));
Console.WriteLine(Descrever('x'));
Console.WriteLine(Descrever(null));

string Descrever(object? item) => item switch
{
    null => "nada",
    int n when n < 0 => $"inteiro negativo {n}",
    int n => $"inteiro {n}",
    double d => $"decimal {d}",
    string s => $"texto \"{s}\"",
    _ => $"outra coisa: {item}",
};
$ dotnet run tipos.cs
inteiro 10
decimal 2,5
texto "pera"
outra coisa: x
nada
item switch {
Examina item e produz um valor (o texto que a função devolve).
null =>
Padrão de constante: casa só com null.
int n
Padrão de tipo: casa com inteiros e cria n.
when n < 0
A guarda: o braço só vale se, além de ser inteiro, n for negativo. Ela pode usar a variável do padrão e qualquer expressão bool.
_ =>
Todo o resto (aqui, o char 'x').
A ordem importa

O caso mais específico vem primeiro. Se int n (sem guarda) viesse antes de int n when n < 0, o segundo nunca seria alcançado. Quando o compilador consegue provar isso, ele recusa o programa com o erro CS8510 (veja "Quando dá errado").


4. Padrões de propriedade: olhar dentro do objeto C# 8

Um padrão de propriedade abre chaves e testa as propriedades do objeto: { Idade: < 18 } lê-se "tem a propriedade Idade menor que 18". Dentro das chaves, cada propriedade recebe outro padrão, de qualquer tipo:

List<Pessoa> pessoas =
[
    new("Ana", 29, new("Recife", "PE")),
    new("Bruno", 15, new("Olinda", "PE")),
    new("Carla", 70, new("Natal", "RN")),
];

foreach (var p in pessoas)
{
    string ingresso = p switch
    {
        { Idade: < 18 } => "meia (estudante)",
        { Idade: >= 65 } => "meia (idoso)",
        _ => "inteira",
    };
    Console.WriteLine($"{p.Nome}: {ingresso}");
}

foreach (var p in pessoas)
{
    if (p is { Endereco.Estado: "PE", Idade: >= 18 })
    {
        Console.WriteLine($"{p.Nome} vota em Pernambuco");
    }
}

if (pessoas[2] is { Nome: var nome, Endereco: { Cidade: var cidade } })
{
    Console.WriteLine($"{nome} mora em {cidade}");
}

record Endereco(string Cidade, string Estado);
record Pessoa(string Nome, int Idade, Endereco Endereco);
$ dotnet run ingresso.cs
Ana: inteira
Bruno: meia (estudante)
Carla: meia (idoso)
Ana vota em Pernambuco
Carla mora em Natal
PadrãoO que testa
{ Idade: < 18 }A propriedade Idade é menor que 18.
{ Endereco.Estado: "PE", Idade: >= 18 }Duas condições ao mesmo tempo (a vírgula é um "e"). Endereco.Estado entra na propriedade de dentro: é o padrão de propriedade estendido C# 10.
{ Nome: var nome, Endereco: { Cidade: var cidade } }O padrão var casa com qualquer valor e o guarda numa variável. As chaves dentro das chaves fazem o mesmo que Endereco.Cidade, no estilo antigo do C# 8.

Um padrão de propriedade também exige que o objeto não seja nulo. Por isso x is { } (chaves vazias) é um jeito curto de dizer "x não é nulo". E uma pessoa cadastrada sem endereço (Endereco nulo) simplesmente não casa com { Endereco.Estado: "PE" }: o teste dá False, em vez de lançar NullReferenceException como p.Endereco.Estado == "PE" lançaria.


5. Padrões posicionais e de tupla C# 8

Um record como record Ponto(int X, int Y) sabe se desconstruir em X e Y, na ordem em que foram declarados. O padrão posicional usa essa ordem: (0, 0) quer dizer "X é 0 e Y é 0".

Console.WriteLine(Descrever(new Ponto(0, 0)));
Console.WriteLine(Descrever(new Ponto(0, 5)));
Console.WriteLine(Descrever(new Ponto(2, 2)));
Console.WriteLine(Descrever(new Ponto(3, 1)));

Console.WriteLine(Quadrante(new Ponto(3, -1)));

string Descrever(Ponto p) => p switch
{
    (0, 0) => "a origem",
    (0, var y) => $"no eixo vertical, altura {y}",
    var (x, y) when x == y => "na diagonal",
    (var x, _) => $"ponto com x = {x}",
};

string Quadrante(Ponto p) => p switch
{
    ( > 0, > 0) => "1º quadrante",
    ( < 0, > 0) => "2º quadrante",
    ( < 0, < 0) => "3º quadrante",
    ( > 0, < 0) => "4º quadrante",
    _ => "em cima de um eixo",
};

record Ponto(int X, int Y);
$ dotnet run ponto.cs
a origem
no eixo vertical, altura 5
na diagonal
ponto com x = 3
4º quadrante
(0, 0)
Um padrão para cada parte, na ordem do record: X é 0 e Y é 0.
(0, var y)
X tem de ser 0; Y pode ser qualquer coisa, e fica guardado em y.
var (x, y) when x == y
Abre o ponto em x e y e usa a guarda para compará-los.
(var x, _)
O _ descarta a parte que não interessa. Este braço casa com qualquer ponto: ele fecha o switch.
( > 0, < 0)
Cada posição aceita qualquer padrão, inclusive os relacionais.

O mesmo vale para tuplas: monte uma tupla na hora com (a, b) e examine as duas coisas juntas. Um jogo de pedra, papel e tesoura fica assim:

Console.WriteLine(Jogar("pedra", "tesoura"));
Console.WriteLine(Jogar("papel", "tesoura"));
Console.WriteLine(Jogar("papel", "papel"));

string Jogar(string a, string b) => (a, b) switch
{
    var (x, y) when x == y => "empate",
    ("pedra", "tesoura") or ("tesoura", "papel") or ("papel", "pedra") => "jogador 1 ganhou",
    _ => "jogador 2 ganhou",
};
$ dotnet run jokenpo.cs
jogador 1 ganhou
jogador 2 ganhou
empate

6. Padrões de lista C# 11

O padrão mais novo olha os itens de um array ou de uma lista, entre colchetes, na ordem. O .. (dois pontos) quer dizer "qualquer quantidade de itens aqui, até nenhum":

int[][] listas = [[], [7], [1, 2], [1, 2, 3, 4], [5, 0, 9]];
foreach (var l in listas)
{
    string d = l switch
    {
        [] => "vazia",
        [var unico] => $"só um: {unico}",
        [1, 2, ..] => "começa com 1, 2",
        [var primeiro, .., var ultimo] => $"de {primeiro} até {ultimo}",
    };
    Console.WriteLine($"[{string.Join(", ", l)}] -> {d}");
}

string[] comando = ["copiar", "a.txt", "b.txt", "c.txt"];
if (comando is ["copiar", var origem, .. var destinos])
{
    Console.WriteLine($"Copiar {origem} para {destinos.Length} lugar(es): {string.Join(" ", destinos)}");
}
$ dotnet run listas.cs
[] -> vazia
[7] -> só um: 7
[1, 2] -> começa com 1, 2
[1, 2, 3, 4] -> começa com 1, 2
[5, 0, 9] -> de 5 até 9
Copiar a.txt para 2 lugar(es): b.txt c.txt
PadrãoCasa com
[]Uma lista vazia.
[var unico]Exatamente um item, guardado em unico.
[1, 2, ..]Começa com 1 e 2; depois, qualquer quantidade (inclusive nada: [1, 2] casou).
[var primeiro, .., var ultimo]Pelo menos dois itens; pega o primeiro e o último.
["copiar", var origem, .. var destinos]O .. var destinos é um padrão de fatia: guarda "o resto" num array novo.

É o mesmo .. dos intervalos (texto[1..]). Repare que este switch não tem _: os quatro braços já cobrem todas as quantidades possíveis (0, 1, e 2 ou mais), e o compilador percebe isso sozinho.


7. Exaustividade: o switch precisa cobrir tudo

Uma expressão switch produz um valor, então precisa de uma resposta para toda entrada possível. Os braços juntos devem ser exaustivos ("esgotar as possibilidades"). Se não forem, o C# compila, mas avisa, e o programa quebra quando chega um valor esquecido:

Console.WriteLine(Nota(8));
Console.WriteLine(Nota(-1));

string Nota(int n) => n switch
{
    >= 7 => "aprovado",
    >= 0 and < 7 => "reprovado",
};
$ dotnet run exaustivo.cs
/home/ana/cs/exaustivo.cs(4,25): warning CS8509: A expressão switch não manipula todos os valores possíveis do tipo de entrada (ela não é exaustiva). Por exemplo, o padrão '-1' não é coberto.
aprovado
Unhandled exception. System.Runtime.CompilerServices.SwitchExpressionException: Non-exhaustive switch expression failed to match its input.
Unmatched value was -1.
   at <PrivateImplementationDetails>.ThrowSwitchExpressionException(Object unmatchedValue)
   at Program.<<Main>$>g__Nota|0_0(Int32 n) in /home/ana/cs/exaustivo.cs:line 4
   at Program.<Main>$(String[] args) in /home/ana/cs/exaustivo.cs:line 2

O aviso CS8509 chega a dar um exemplo do que falta: "o padrão '-1' não é coberto". O programa rodou mesmo assim, mostrou aprovado e, quando chegou o -1, lançou a exceção SwitchExpressionException ("a expressão switch não exaustiva não casou com a entrada"), com código de saída 134. Leve esse aviso a sério: acrescente o braço que falta ou um _ => … no fim.

E com classes e records?

Com uma hierarquia (por exemplo, abstract record Forma com Circulo e Retangulo), o C# 14 não sabe que não existem outras formas: qualquer pessoa pode criar mais uma. Mesmo tratando Circulo e Retangulo, o compilador dá CS8509 dizendo que o padrão '_' não é coberto. Por isso, nesses casos, termine com _ => … (de preferência lançando uma exceção que diga o que aconteceu). O mesmo vale para enums: um (Cor)7 é um valor válido do tipo, e o aviso é o CS8524.


Antes × depois

SituaçãoAntesDepoisDesde
Testar o tipo e usarif (o is string) { var s = (string)o; … }if (o is string s) { … }C# 7
Não é nuloif (x != null)if (x is not null)C# 9
Está numa faixan >= 1 && n <= 10n is >= 1 and <= 10C# 9
É um de vários valoresd == 6 || d == 7d is 6 or 7C# 9
Testar propriedadesp != null && p.Idade < 18p is { Idade: < 18 }C# 8
Propriedade de dentrop.Endereco != null && p.Endereco.Estado == "PE"p is { Endereco.Estado: "PE" }C# 10
Duas coisas juntasif dentro de if(a, b) switch { ("pedra", "tesoura") => … }C# 8
Começo de uma listal.Length >= 2 && l[0] == 1 && l[1] == 2l is [1, 2, ..]C# 11

Qual usar?

Você quer…Use
Testar uma condição num ifis com o padrão
Escolher um valor entre vários casosExpressão switch com padrões
Uma condição que nenhum padrão expressa (comparar duas variáveis, chamar um método)Guarda when
Testar propriedades de um objeto pelo nomePadrão de propriedade { Nome: … }
Testar as partes de um record ou de uma tupla pela posiçãoPadrão posicional (…, …)
Testar os itens de um array ou de uma listaPadrão de lista […]
Comparar com nullis null / is not null

Quando dá errado

Usar a variável de padrão onde o teste pode ter falhado

Este código tem um erro de propósito.
object coisa = "uva";
if (coisa is string s)
{
    Console.WriteLine("É texto");
}
Console.WriteLine(s.Length);
$ dotnet run fora.cs
/home/ana/cs/fora.cs(6,19): error CS0165: Uso de variável local não atribuída "s"

Ocorreu uma falha no build. Corrija os erros de build e execute novamente.

O que quer dizer: "uso de variável local não atribuída" (Use of unassigned local variable 's'). A variável s existe na linha 6 (ela vale no bloco em volta do if), mas só ganha valor se o teste der certo; ali o compilador não tem essa certeza. O mesmo erro aparece com if (coisa is string s || s.Length > 0), porque o lado direito do || roda justamente quando o teste falhou. Como consertar: use s só dentro do if (ou depois de um &&), ou inverta o teste: if (coisa is not string s) return;, como na BoasVindas.

O mesmo nome dentro e fora do if

Este código tem um erro de propósito.
object coisa = "banana";
if (coisa is string)
{
    string texto = (string)coisa;
    Console.WriteLine(texto);
}
if (coisa is string texto)
{
    Console.WriteLine(texto);
}
$ dotnet run nome.cs
/home/ana/cs/nome.cs(4,12): error CS0136: Um local ou um parâmetro denominado "texto" não pode ser declarado neste escopo porque esse nome é usado em um escopo delimitador de local para definir um local ou parâmetro

Ocorreu uma falha no build. Corrija os erros de build e execute novamente.

O que quer dizer: o texto do padrão, na linha 7, pertence ao bloco de fora (aqui, o programa inteiro). Então o texto da linha 4, dentro das chaves do primeiro if, tenta usar um nome que já é de um escopo que o envolve. Repare que o erro aponta a linha 4, não a 7. Como consertar: use nomes diferentes, como fizemos com texto2, ou coloque cada teste dentro de um método próprio.

Caso geral antes do caso específico

Este código tem um erro de propósito.
Console.WriteLine(Nota(8));

string Nota(int n) => n switch
{
    >= 0 => "válida",
    >= 7 => "aprovado",
    _ => "inválida",
};
$ dotnet run ordem.cs
/home/ana/cs/ordem.cs(6,5): error CS8510: O padrão não está acessível. Ele já foi manipulado por um ARM anterior da expressão do comutador ou não é possível fazer a correspondência.

Ocorreu uma falha no build. Corrija os erros de build e execute novamente.

O que quer dizer: a tradução é estranha ("ARM" é arm, braço, e "comutador" é o switch). Em inglês: The pattern has already been handled by a previous arm of the switch expression. Todo número >= 7 também é >= 0, então o braço da linha 6 nunca seria usado. Como consertar: do mais específico para o mais geral: >= 7 primeiro, depois >= 0.

Switch sem cobrir tudo

Aviso CS8509 ao compilar e SwitchExpressionException ao rodar (seção 7). Como consertar: acrescente o braço que falta (o aviso sugere um exemplo) ou um _ => … no fim.


Referência

PadrãoExemploCasa quandoDesde
Tipo (declaração)o is string so é string (cria s)C# 7
Constantex is 0, x is null, x is "PE"É igual ao valorC# 7
var{ Nome: var n }Sempre (guarda o valor)C# 7
Descarte_SempreC# 8
Propriedade{ Idade: < 18 }, { }Não é nulo e as propriedades casamC# 8
Propriedade estendida{ Endereco.Estado: "PE" }A propriedade de dentro casaC# 10
Posicional / tupla(0, var y), ("pedra", "papel")As partes casam, na ordemC# 8
Relacional>= 7, < 0A comparação é verdadeiraC# 9
Lógico>= 0 and <= 10, 6 or 7, not nullCombinação de padrõesC# 9
Lista[], [1, ..], [var a, .., var z]Os itens casam, na ordemC# 11
Fatia[_, .. var resto]Guarda o trecho do meioC# 11
Guardaint n when n > 0 => …O padrão casa e a condição é verdadeiraC# 7

Exercícios

1. Complete para testar se o é um string e chamá-lo de nome:

if (o ) { Console.WriteLine(nome.Length); }

2. Complete o padrão para "n está entre 1 e 10, inclusive":

if (n is >= 1 <= 10)

3. Complete o padrão de lista que casa com "qualquer lista que comece com 0":

if (numeros is )

4. O que p is { Idade: >= 18 } faz se p for null?

5. Uma expressão switch deu o aviso CS8509. O que acontece ao rodar?

6. Por que if (o is string s || s.Length > 0) dá CS0165?

Missão final: a loja da Ana aceita Pix e cartão. Complete o switch com padrões: um Pix com a chave vazia vira Pix sem chave!; os outros Pix viram Pix de 50,00 para ana@email.com; um Cartao com 1 parcela vira Cartão à vista: 80,00; com mais parcelas, Cartão em 4x de 30,00 (o valor dividido pelas parcelas). Use um padrão posicional para o cartão: Cartao(1, var valor).

List<Pagamento> pagamentos = [new Pix("ana@email.com", 50.00m), new Cartao(1, 80.00m), new Cartao(4, 120.00m), new Pix("", 10.00m)];
foreach (var p in pagamentos) Console.WriteLine(Descrever(p));

string Descrever(Pagamento p) => p switch
{
    // escreva os braços aqui
    _ => "?",
};

abstract record Pagamento;
record Pix(string Chave, decimal Valor) : Pagamento;
record Cartao(int Parcelas, decimal Valor) : Pagamento;
List<Pagamento> pagamentos = [new Pix("ana@email.com", 50.00m), new Cartao(1, 80.00m), new Cartao(4, 120.00m), new Pix("", 10.00m)];
foreach (var p in pagamentos) Console.WriteLine(Descrever(p));

string Descrever(Pagamento p) => p switch
{
    Pix { Chave: "" } => "Pix sem chave!",
    Pix(var chave, var valor) => $"Pix de {valor} para {chave}",
    Cartao(1, var valor) => $"Cartão à vista: {valor}",
    Cartao(var n, var valor) => $"Cartão em {n}x de {valor / n}",
    _ => "?",
};

abstract record Pagamento;
record Pix(string Chave, decimal Valor) : Pagamento;
record Cartao(int Parcelas, decimal Valor) : Pagamento;

A solução, na máquina da Ana. O braço _ => "?" continua lá: sem ele, o compilador daria o CS8509, porque alguém poderia criar outro tipo de Pagamento (seção 7).

$ dotnet run pagamentos.cs
Pix de 50,00 para ana@email.com
Cartão à vista: 80,00
Cartão em 4x de 30,00
Pix sem chave!

Resumo

  • Um padrão testa a forma de um valor e já extrai as partes, sem cast. Ele aparece no is e nos braços do switch.
  • o is string s C# 7 cria s, que só pode ser lida onde o teste certamente deu certo (senão, CS0165). A variável vale no bloco em volta do if (nome repetido: CS0136).
  • is null, is not null, >= 7, and, or, not C# 9 deixam testes de faixa e de valores curtos e legíveis.
  • { Idade: < 18 } testa propriedades ({ A.B: … } desde o C# 10); (0, var y) testa partes de records e tuplas; [1, .., var ultimo] C# 11 testa os itens de listas.
  • A guarda when acrescenta uma condição; os braços são testados de cima para baixo, do específico para o geral (senão, CS8510).
  • Uma expressão switch precisa ser exaustiva: o aviso CS8509 vira SwitchExpressionException ao rodar. Com classes e enums, termine com _ => ….

Próxima aula: Sobrecarga de Operadores, para ensinar o + e o == a funcionar com as suas classes.