Esse é o 4º post da série sobre C# 7. Pra acompanhar a série você pode seguir a tag C#7 no blog ou voltar no post que agrega a série.

Vamos dar uma olhada em Pattern Matching, algo que costuma estar bastante presente em linguagens funcionais, e que agora chega, em sua primeira encarnação ao C#, em sua sétima versão. Já há uma série de ideias previstas para expandir as construções que vou explorar nesse post em versões futuras do C# depois da sétima, mostrando que o C# segue ganhando bastante influência das funcionais.

A funcionalidade está estável, tanto que está presente no último Preview (4) do Visual Studio “15” (não confunda com o Visual Studio 2015, que é a versão 14). Mas ainda vai mudar um pouco até a release final.

Você pode ler o documento no github que sugere pattern matching no repositório do Roslyn, com o nome “Pattern Matching for C#”, e a funcionalidade é acompanhada por uma tag chamada “New Language Feature – Pattern Matching” que agrega sugestões em volta da proposta principal. A discussão em torno da proposta acontece na issue #10153, no Github também.

Dois tipos de pattern matching

Pattern matching, simplificando bastante, é a ideia de comparar um valor com um padrão. No caso do C# ele aparece de duas formas nesse momento: com uma aplicação do operador “is” e nos “case”s de uma declaração “switch”, que agora podem ter padrões, além de constantes. E os padrões aceitáveis, nesse momento, estão restritos a valores constantes, tipos, e var. Ainda é bem pouco, mas é um começo, vou detalhar eles melhor adiante.

Expressões com “is”

Você já pode validar se um valor é de um determinado tipo com “is”:

if (x is Program) { ... }

Também pode validar um valor contra constantes com “==”:

if (x == null) { ... }

Mas não podia juntar os dois, ou seja, usar o “is” para validar uma constante. Agora com C# 7 poderá. Nesse caso, null:

if (x is null) { ... }

Ou, validar contra um número, por exemplo:

if (x is 3) { ... }

Melhor que tudo isso, agora você pode atribuir esse valor a uma variável, por exemplo:

if (x is int y) { ... }

Nesse contexto, y está em contexto somente dentro do bloco do if. Mas isso vai mudar, e em breve y estará em contexto como se fosse declarado onde está o if.

Dessa forma, você pode construir o método de soma mais estúpido do mundo:

int? SomaSete(object x, object y) => ((x is 3) && y is 4) ? 7 : (int?)null;

Esse método só consegue somar se x for 3, e y for 4.

Note que a verificação de x acontece entre parênteses, porque o operador && tem precedência, o que causaria um erro sem os parênteses.

Uma vez declarada a variável ela pode ser usada no contexto, por exemplo, com métodos dentro do mesmo bloco de condições de um if:

static int? Soma(object x, object y)
{
    if ((x is null) || y is null) return null;
    if ((x is int i || (x is string s && int.TryParse(s, out i))) &&
        (y is int j || (y is string t && int.TryParse(t, out j))))
        return i + j;
    return null;
}

Nesse caso, “i” é declarado na primeira condição do if, em seguida fica em contexto para o uso do TryParse do lado direito do ||, já usando o valor “s” verificado logo antes. O mesmo acontece com y, em estrutura idêntica de verificação.

Switch case com tipos e condições

O switch foi expandido para permitir qualquer valor, não somente alguns tipos tinham constantes anteriormente (bool, char, string, inteiros, e enum  – e seus nuláveis). Nem mesmo float ou DateTime eram permitidos.

Agora é possível verificar o tipo, e em seguida fazer verificações adicionais. Por exemplo, aqui verificamos se uma forma é um circulo, um retangulo, um quadrado, nulo, ou alguma outra coisa:

static void MostraForma(Forma forma)
{
    switch (forma)
    {
        case Circulo c:
            WriteLine($"Circulo com raio {c.Raio}");
            break;
        case Retangulo q when (q.Largura == q.Altura):
            WriteLine($"{q.Largura} x {q.Altura} quadrado");
            break;
        case Retangulo r:
            WriteLine($"{r.Largura} x {r.Altura} retangulo");
            break;
        default:
            WriteLine($"<tipo desconhecido ${forma.GetType().FullName}>");
            break;
        case null:
            WriteLine("Nulos sao feios");
            break;
    }
}

Nesse caso, o tipo de “forma” é o argumento para cada case. E podemos fazer verificações adicionais, note que no segundo case, quando verificamos se é um retângulo, checamos em seguida se os lados são iguais, assumindo um quadrado então.

Um ponto importantíssimo é que a ordem dos cases importa agora. Assim, se a checagem por retângulo acontecesse antes, nunca conseguiríamos encontrar um quadrado. Mas o Visual Studio vai te mostrar que isso é um problema:

Erro ao errar a ordem das condições

Isso vai gerar o erro CS8120, que impedirá o código de compilar.

Já o caso “default”, mesmo se colocado antes, é sempre avaliado por último. Nesse caso, o ideal seria mover ele pro final, pra evitar confusão.

Veja também que ainda podemos bater com constantes, nesse caso foi usado o nulo, o que é sempre uma boa ideia, pra evitar exceções de null reference nos cases seguintes.

Futuro

Há outras funcionalidades muito interessantes listadas nos documentos que linkei acima, além do documento de futures que está no repo. Vou citar algumas que acho especialmente interessantes e/ou que podem entrar ainda no C# 7.

Primeiro, dê uma olhada em records, algo que quase entrou em C# 6, e acabou cortado, e é lindo. É possível que eu discuta ele no futuro, mas não sabemos ainda se vai entrar nem no 7, parece que não. É uma feature irmã a de pattern matching e que, se for entregue, vai deixar o pattern matching do C# ainda mais poderoso.

Está em discussão a ideia de criarem switch expressions, em vez de apenas switch statements. Em outras palavras, um switch poderia produzir um valor, por exemplo:

var area = forma switch (
    case Linha l: 0,
    case Retangulo r: r.Largura * r.Altura,
    case Circulo c: Math.PI * c.Raio * c.Raio,
    case *: throw new ApplicationException()
);

E notaram o “*”? É outro ponto em discussão. Ainda não está certo se vai entrar, mas há chance.

Um outro ponto que seria incrível se fosse entregue ainda nessa versão é a possibilidade de fazer overload do operador “is”, por exemplo, tenho uma classe Venda, e quero saber se foi uma venda especial, posso saber então as condições e o vendedor desta venda.  O operador ficaria assim.

public static class Especial
{
    public static bool operator is(Venda v, out Condicao condicao, out Vendedor vendedor)
    {
        //obtem a condicao e o vendedor em algum lugar usando a venda...
        condicao = ...;
        vendedor = ...;
    }
}

Digamos que em outro ponto eu não precise do vendedor, mas ainda quero a condição, e já tenho a venda na variável “venda”:

var venda = new Venda();
if (venda is Especial(var condicao, *))
    WriteLine(condicao.Descricao);

Lembram que “var” bate com qualquer valor? Nesse caso, estou aceitando qualquer condição, e estou ignorando o vendedor com “*”. Mas, vamos dizer que a condição seja um enumerador, eu poderia fazer algo assim:

var venda = new Venda();
if (venda is Especial(Condicao.ComDesconto, *)) { ... }

Notam o poder desse tipo de sintaxe? Infelizmente ainda não sabemos se vai dar tempo de ela vai entrar nessa versão.

Bugs

Sim, há bugs ainda, a versão 7 do compilador ainda está sendo escrita. Encontrei um interessante, ao analisar a IL desse método, descobri que eles ainda não terminaram de implementar. Ele vai um loop que vai iterar infinitamente enquanto x for menor 5.

static void Bugado(double x)
{
    switch (x)
    {
        case double d when (d > 5):
            WriteLine(d + 2);
            break;
        case double d when (d > 3):
            WriteLine(d + 1);
            break;
        case double d:
            WriteLine(d + 3);
            break;
        default:
            WriteLine(4);
            break;
    }
}

Mas isso faz parte. E vocês, encontraram outros bugs? Mas fica a dica, não use o VS “15” para nada sério, imaginam o perigo?

Conclusão

Há ainda um bom caminho pra evoluir em pattern matching, tenho muitas ideias que imagino que eles poderiam perseguir, além das que eles já listaram. O futuro é cada dia mais bonito no C#. O que vocês acharam? Já conheciam pattern matching? De qual linguagem? Contem aí nos comentários!

Giovanni Bassi

Arquiteto e desenvolvedor, agilista, escalador, provocador. É fundador e CSA da Lambda3. Programa porque gosta. Acredita que pessoas autogerenciadas funcionam melhor e por acreditar que heterarquia é mais eficiente que hierarquia. Foi reconhecido Microsoft MVP há mais de dez anos, dos mais de vinte que atua no mercado. Já palestrou sobre .NET, Rust, microsserviços, JavaScript, TypeScript, Ruby, Node.js, Frontend e Backend, Agile, etc, no Brasil, e no exterior. Liderou grupos de usuários em assuntos como arquitetura de software, Docker, e .NET.