Correspondência padrãoPattern Matching

Os padrões testam um valor que tem uma determinada forma e podem extrair informações do valor quando ele tem a forma correspondente.Patterns test that a value has a certain shape, and can extract information from the value when it has the matching shape. A correspondência de padrões fornece uma sintaxe mais concisa para algoritmos que você já usa atualmente.Pattern matching provides more concise syntax for algorithms you already use today. Você já cria algoritmos de correspondência de padrões usando a sintaxe existente.You already create pattern matching algorithms using existing syntax. Você escreve instruções if ou switch que testam os valores.You write if or switch statements that test values. Então, quando essas instruções correspondem, você extrai e usa informações desse valor.Then, when those statements match, you extract and use information from that value. Os novos elementos de sintaxe são extensões para as instruções com as quais você já está familiarizado: is e switch.The new syntax elements are extensions to statements you're already familiar with: is and switch. Essas novas extensões combinam o teste de um valor e a extração dessas informações.These new extensions combine testing a value and extracting that information.

Neste artigo, vamos examinar a nova sintaxe para mostrar a você como ela possibilita um código conciso e legível.In this article, we'll look at the new syntax to show you how it enables readable, concise code. A correspondência de padrões habilita expressões em que os dados e o código são separados, diferente dos designs orientados a objeto em que os dados e os métodos que os manipulam estão intimamente ligados.Pattern matching enables idioms where data and the code are separated, unlike object-oriented designs where data and the methods that manipulate them are tightly coupled.

Para ilustrar essas novas expressões, vamos trabalhar com estruturas que representam formas geométricas usando instruções de correspondência de padrões.To illustrate these new idioms, let's work with structures that represent geometric shapes using pattern matching statements. Provavelmente você está familiarizado com a criação de hierarquias de classe com a criação de métodos virtuais e métodos substituídos para personalizar o comportamento do objeto com base no tipo de tempo de execução do objeto.You're probably familiar with building class hierarchies and creating virtual methods and overridden methods to customize object behavior based on the runtime type of the object.

Essas técnicas não são possíveis para dados não estruturados em uma hierarquia de classe.Those techniques aren't possible for data that isn't structured in a class hierarchy. Quando os dados e métodos são separados, você precisa de outras ferramentas.When data and methods are separate, you need other tools. Os novos constructos de correspondência de padrões permitem uma sintaxe mais clara para examinar dados e manipular o fluxo de controle com base em qualquer condição desses dados.The new pattern matching constructs enable cleaner syntax to examine data and manipulate control flow based on any condition of that data. Você já escreve instruções if e switch que testam o valor da variável.You already write if statements and switch that test a variable's value. Você escreve instruções is que testam o tipo da variável.You write is statements that test a variable's type. A correspondência de padrões adiciona novos recursos a essas instruções.Pattern matching adds new capabilities to those statements.

Neste artigo, você criará um método que calcula a área de diferentes formas geométricas.In this article, you'll build a method that computes the area of different geometric shapes. Mas fará isso sem recorrer às técnicas orientadas a objeto e sem criar uma hierarquia de classe para as diferentes formas.But, you'll do it without resorting to object-oriented techniques and building a class hierarchy for the different shapes. Em vez disso, você usará correspondência de padrões.You'll use pattern matching instead. Conforme percorrer esse exemplo, contraste esse código com como ele seria estruturado em uma hierarquia de objeto.As you go through this sample, contrast this code with how it would be structured as an object hierarchy. Quando os dados que você deve consultar e manipular não são uma hierarquia de classe, a correspondência de padrões permite designs elegantes.When the data you must query and manipulate isn't a class hierarchy, pattern matching enables elegant designs.

Em vez de iniciar com uma definição de forma abstrata e adicionar classes de forma específicas diferentes, vamos começar com os dados simples, apenas definições de dados para cada uma das formas geométricas:Rather than starting with an abstract shape definition and adding different specific shape classes, let's start instead with simple data only definitions for each of the geometric shapes:

public class Square
{
    public double Side { get; }

    public Square(double side)
    {
        Side = side;
    }
}
public class Circle
{
    public double Radius { get; }

    public Circle(double radius)
    {
        Radius = radius;
    }
}
public struct Rectangle
{
    public double Length { get; }
    public double Height { get; }

    public Rectangle(double length, double height)
    {
        Length = length;
        Height = height;
    }
}
public class Triangle
{
    public double Base { get; }
    public double Height { get; }

    public Triangle(double @base, double height)
    {
        Base = @base;
        Height = height;
    }
}

Dessas estruturas, vamos escrever um método que calcula a área de uma forma.From these structures, let's write a method that computes the area of some shape.

A expressão padrão de tipo isThe is type pattern expression

Antes do C# 7.0, você precisaria testar cada tipo de uma série de instruções if e is:Before C# 7.0, you'd need to test each type in a series of if and is statements:

public static double ComputeArea(object shape)
{
    if (shape is Square)
    {
        var s = (Square)shape;
        return s.Side * s.Side;
    } 
    else if (shape is Circle)
    {
        var c = (Circle)shape;
        return c.Radius * c.Radius * Math.PI;
    }
    // elided
    throw new ArgumentException(
        message: "shape is not a recognized shape",
        paramName: nameof(shape));
}

O código acima é uma expressão clássica do padrão de tipo: Você está testando uma variável para determinar seu tipo e adotando uma ação diferente com base nesse tipo.That code above is a classic expression of the type pattern: You're testing a variable to determine its type and taking a different action based on that type.

Esse código se torna mais simples usando extensões para a expressão is para atribuir uma variável se o teste tiver êxito:This code becomes simpler using extensions to the is expression to assign a variable if the test succeeds:

public static double ComputeAreaModernIs(object shape)
{
    if (shape is Square s)
        return s.Side * s.Side;
    else if (shape is Circle c)
        return c.Radius * c.Radius * Math.PI;
    else if (shape is Rectangle r)
        return r.Height * r.Length;
    // elided
    throw new ArgumentException(
        message: "shape is not a recognized shape",
        paramName: nameof(shape));
}

Nesta versão atualizadas, a expressão is testa a variável e a atribui a uma nova variável do tipo adequado.In this updated version, the is expression both tests the variable and assigns it to a new variable of the proper type. Além disso, observe que essa versão inclui o tipo Rectangle, que é um struct.Also, notice that this version includes the Rectangle type, which is a struct. A nova expressão is funciona com tipos de valor, bem como com tipos de referência.The new is expression works with value types as well as reference types.

As regras da linguagem para expressões de correspondência de padrões ajudam a evitar usar incorretamente os resultados de uma expressão de correspondência.Language rules for pattern matching expressions help you avoid misusing the results of a match expression. No exemplo acima, as variáveis s, c e r estão somente no escopo e são atribuídas definitivamente quando as expressões de correspondência de padrão têm resultados true.In the example above, the variables s, c, and r are only in scope and definitely assigned when the respective pattern match expressions have true results. Se você tentar usar qualquer variável em outro local, seu código gerará erros de compilador.If you try to use either variable in another location, your code generates compiler errors.

Vamos examinar ambas as regras detalhadamente, começando com o escopo.Let's examine both of those rules in detail, beginning with scope. A variável c está no escopo somente no branch else da primeira instrução if.The variable c is in scope only in the else branch of the first if statement. A variável s está no escopo no método ComputeAreaModernIs.The variable s is in scope in the method ComputeAreaModernIs. Isso ocorre porque cada branch de uma instrução if estabelece um escopo separado para as variáveis.That's because each branch of an if statement establishes a separate scope for variables. No entanto, a instrução if em si não.However, the if statement itself doesn't. Isso significa que as variáveis declaradas na instrução if estão no mesmo escopo da instrução if (o método nesse caso). Esse comportamento não é específico para correspondência de padrões, mas é o comportamento definido para escopos de variável e instruções if e else.That means variables declared in the if statement are in the same scope as the if statement (the method in this case.) This behavior isn't specific to pattern matching, but is the defined behavior for variable scopes and if and else statements.

As variáveis c e s são atribuídas quando as respectivas instruções if são verdadeiras por causa do mecanismo atribuído definitivamente quando elas são verdadeiras.The variables c and s are assigned when the respective if statements are true because of the definitely assigned when true mechanism.

Dica

Os exemplos neste tópico usam o constructo recomendado quando uma expressão is de correspondência de padrão atribui definitivamente a variável correspondente no branch true da instrução if.The samples in this topic use the recommended construct where a pattern match is expression definitely assigns the match variable in the true branch of the if statement. Você poderia reverter a lógica dizendo que if (!(shape is Square s)) e a variável s seriam definitivamente atribuídas apenas no branch false.You could reverse the logic by saying if (!(shape is Square s)) and the variable s would be definitely assigned only in the false branch. Embora isso seja válido no C#, não é recomendável porque é mais confuso para acompanhar a lógica.While this is valid C#, it is not recommended because it is more confusing to follow the logic.

Essas regras significam que é improvável que você acesse acidentalmente o resultado de uma expressão de correspondência de padrão quando esse padrão não foi atendido.These rules mean that you're unlikely to accidentally access the result of a pattern match expression when that pattern wasn't met.

Usando instruções switch de correspondência de padrõesUsing pattern matching switch statements

Com o tempo, talvez seja necessário dar suporte a outros tipos de forma.As time goes on, you may need to support other shape types. Conforme o número de condições sendo testadas aumenta, você descobrirá que usar as expressões de correspondência de padrões is pode se tornar complicado.As the number of conditions you're testing grows, you'll find that using the is pattern matching expressions can become cumbersome. Além de exigirem instruções if em cada tipo que você deseja verificar, as expressões is são limitadas a testar se a entrada corresponder a um único tipo.In addition to requiring if statements on each type you want to check, the is expressions are limited to testing if the input matches a single type. Nesse caso, você descobrirá que as expressões de correspondência de padrões switch se tornam uma opção melhor.In this case, you'll find that the switch pattern matching expressions becomes a better choice.

A instrução switch tradicional era uma expressão padrão: ela dava suporte ao padrão de constante.The traditional switch statement was a pattern expression: it supported the constant pattern. Você poderia comparar uma variável a qualquer constante usada em uma instrução case:You could compare a variable to any constant used in a case statement:

public static string GenerateMessage(params string[] parts)
{
    switch (parts.Length)
    {
        case 0:
            return "No elements to the input";
        case 1:
            return $"One element: {parts[0]}";
        case 2:
            return $"Two elements: {parts[0]}, {parts[1]}";
        default:
            return $"Many elements. Too many to write";
    }
}

O único padrão com suporte pela instrução switch era o padrão de constante.The only pattern supported by the switch statement was the constant pattern. Ele era ainda mais limitado a tipos numéricos e ao tipo string.It was further limited to numeric types and the string type. Essas restrições foram removidas e agora você pode escrever uma instrução switch usando o padrão de tipo:Those restrictions have been removed, and you can now write a switch statement using the type pattern:

public static double ComputeAreaModernSwitch(object shape)
{
    switch (shape)
    {
        case Square s:
            return s.Side * s.Side;
        case Circle c:
            return c.Radius * c.Radius * Math.PI;
        case Rectangle r:
            return r.Height * r.Length;
        default:
            throw new ArgumentException(
                message: "shape is not a recognized shape",
                paramName: nameof(shape));
    }
}

A instrução switch da correspondência de padrões usa a sintaxe familiar para os desenvolvedores que usaram a instrução switch de estilo C tradicional.The pattern matching switch statement uses familiar syntax to developers who have used the traditional C-style switch statement. Cada case é avaliada e o código sob a condição que corresponde a variável de entrada é executado.Each case is evaluated and the code beneath the condition that matches the input variable is executed. A execução de código não pode "passar" de uma expressão case para a seguinte, a sintaxe da instrução case requer que cada case termine com um break, return ou goto.Code execution can't "fall through" from one case expression to the next; the syntax of the case statement requires that each case end with a break, return, or goto.

Observação

As instruções goto para pular para outro rótulo são válidas somente para o padrão de constante, a instrução switch clássica.The goto statements to jump to another label are valid only for the constant pattern (the classic switch statement).

Há novas regras importantes regendo a instrução switch.There are important new rules governing the switch statement. As restrições no tipo da variável na expressão switch foram removidas.The restrictions on the type of the variable in the switch expression have been removed. Qualquer tipo, como object neste exemplo, pode ser usado.Any type, such as object in this example, may be used. As expressões case não são mais limitadas a valores de constantes.The case expressions are no longer limited to constant values. Remover essa limitação significa que reordenar seções switch pode alterar o comportamento do programa.Removing that limitation means that reordering switch sections may change a program's behavior.

Quando limitado a valores de constantes, no máximo um rótulo case poderia corresponder ao valor da expressão switch.When limited to constant values, no more than one case label could match the value of the switch expression. Combine isso com a regra de que cada seção switch não deve passar para a próxima seção e isso resulta em que as seções switch poderiam ser reorganizadas em qualquer ordem sem afetar o comportamento.Combine that with the rule that every switch section must not fall through to the next section, and it followed that the switch sections could be rearranged in any order without affecting behavior. Agora, com expressões switch mais generalizadas, a ordem de cada seção é importante.Now, with more generalized switch expressions, the order of each section matters. As expressões switch são avaliadas na ordem textual.The switch expressions are evaluated in textual order. A execução transfere o primeiro rótulo switch que corresponde à expressão switch.Execution transfers to the first switch label that matches the switch expression.
O case default será executado somente se nenhum outro rótulo case corresponder.The default case will only be executed if no other case labels match. O case default é avaliado por último, independentemente de sua ordem textual.The default case is evaluated last, regardless of its textual order. Se não houver nenhum case default e nenhuma das outras instruções case corresponder, a execução continuará na instrução após a instrução switch.If there's no default case, and none of the other case statements match, execution continues at the statement following the switch statement. Nenhum dos códigos de rótulos case será executado.None of the case labels code is executed.

Cláusulas when em expressões casewhen clauses in case expressions

Você pode criar cases especiais para essas formas que têm área 0 usando uma cláusula when no rótulo case.You can make special cases for those shapes that have 0 area by using a when clause on the case label. Um quadrado com um comprimento do lado 0 ou um círculo com um raio 0 tem uma área 0.A square with a side length of 0, or a circle with a radius of 0 has a 0 area. Você especifica essa condição usando uma cláusula when no rótulo case:You specify that condition using a when clause on the case label:

public static double ComputeArea_Version3(object shape)
{
    switch (shape)
    {
        case Square s when s.Side == 0:
        case Circle c when c.Radius == 0:
            return 0;

        case Square s:
            return s.Side * s.Side;
        case Circle c:
            return c.Radius * c.Radius * Math.PI;
        default:
            throw new ArgumentException(
                message: "shape is not a recognized shape",
                paramName: nameof(shape));
    }
}

Essa alteração demonstra alguns pontos importantes sobre a nova sintaxe.This change demonstrates a few important points about the new syntax. Primeiro, vários rótulos case podem ser aplicados a uma seção switch.First, multiple case labels can be applied to one switch section. O bloco de instrução é executado quando qualquer um desses rótulos é true.The statement block is executed when any of those labels is true. Neste exemplo, se a expressão switch é um círculo ou um quadrado com área 0, o método retorna a constante 0.In this instance, if the switch expression is either a circle or a square with 0 area, the method returns the constant 0.

Este exemplo apresenta duas variáveis diferentes nos dois rótulos case para o primeiro bloco switch.This example introduces two different variables in the two case labels for the first switch block. Observe que as instruções neste bloco switch não usam as variáveis c (para o círculo) ou s (para o quadrado).Notice that the statements in this switch block don't use either the variables c (for the circle) or s (for the square). Nenhuma dessas variáveis é atribuída definitivamente nesse bloco switch.Neither of those variables is definitely assigned in this switch block. Se algum desses casos corresponder, claramente uma das variáveis foi atribuída.If either of these cases match, clearly one of the variables has been assigned. No entanto, é impossível dizer qual foi atribuída em tempo de compilação, porque ambos os casos poderiam corresponder em tempo de execução.However, it's impossible to tell which has been assigned at compile time, because either case could match at runtime. Por esse motivo, na maioria das vezes que você usar vários rótulos case para o mesmo bloco, não introduzirá uma nova variável na instrução case ou apenas usará a variável na cláusula when.For that reason, most times when you use multiple case labels for the same block, you won't introduce a new variable in the case statement, or you'll only use the variable in the when clause.

Depois de adicionar essas formas com a área 0, vamos adicionar mais alguns tipos de forma: um retângulo e um triângulo:Having added those shapes with 0 area, let's add a couple more shape types: a rectangle and a triangle:

public static double ComputeArea_Version4(object shape)
{
    switch (shape)
    {
        case Square s when s.Side == 0:
        case Circle c when c.Radius == 0:
        case Triangle t when t.Base == 0 || t.Height == 0:
        case Rectangle r when r.Length == 0 || r.Height == 0:
            return 0;

        case Square s:
            return s.Side * s.Side;
        case Circle c:
            return c.Radius * c.Radius * Math.PI;
        case Triangle t:
            return t.Base * t.Height / 2;
        case Rectangle r:
            return r.Length * r.Height;
        default:
            throw new ArgumentException(
                message: "shape is not a recognized shape",
                paramName: nameof(shape));
    }
}

Esse conjunto de alterações adiciona rótulos case para o caso de degeneração e rótulos e blocos para cada uma das novas formas.This set of changes adds case labels for the degenerate case, and labels and blocks for each of the new shapes.

Por fim, você pode adicionar um case null para garantir que o argumento não seja null:Finally, you can add a null case to ensure the argument isn't null:

public static double ComputeArea_Version5(object shape)
{
    switch (shape)
    {
        case Square s when s.Side == 0:
        case Circle c when c.Radius == 0:
        case Triangle t when t.Base == 0 || t.Height == 0:
        case Rectangle r when r.Length == 0 || r.Height == 0:
            return 0;

        case Square s:
            return s.Side * s.Side;
        case Circle c:
            return c.Radius * c.Radius * Math.PI;
        case Triangle t:
            return t.Base * t.Height / 2;
        case Rectangle r:
            return r.Length * r.Height;
        case null:
            throw new ArgumentNullException(paramName: nameof(shape), message: "Shape must not be null");
        default:
            throw new ArgumentException(
                message: "shape is not a recognized shape",
                paramName: nameof(shape));
    }
}

O comportamento especial para o padrão null é interessante porque a constante null no padrão não tem um tipo, mas pode ser convertida em qualquer tipo de referência ou tipo que permite valor nulo.The special behavior for the null pattern is interesting because the constant null in the pattern doesn't have a type but can be converted to any reference type or nullable type. Em vez de converter um null em qualquer tipo, a linguagem define que um null valor não será correspondente ao padrão de qualquer tipo, independentemente do tipo de tempo de compilação da variável.Rather than convert a null to any type, the language defines that a null value won't match any type pattern, regardless of the compile-time type of the variable. Esse comportamento torna o novo padrão de tipo baseado em switch consistente com a instrução is: instruções is sempre retornam false quando o valor sendo verificado é null.This behavior makes the new switch based type pattern consistent with the is statement: is statements always return false when the value being checked is null. Isso também é mais simples: depois de verificar o tipo, não é necessário fazer uma verificação adicional de nulos.It's also simpler: once you've checked the type, you don't need an additional null check. Você pode ver isso pelo fato de que não há nenhuma verificação de nulos em nenhum dos blocos de casos dos exemplos acima: elas não são necessárias, já que a correspondência do padrão de tipo assegura um valor não nulo.You can see that from the fact that there are no null checks in any of the case blocks of the samples above: they aren't necessary, since matching the type pattern guarantees a non-null value.

var declarações em expressões casevar declarations in case expressions

A introdução de var como uma das expressões de correspondência introduz novas regras à correspondência de padrão.The introduction of var as one of the match expressions introduces new rules to the pattern match.

A primeira regra é que a declaração var segue as regras de inferência de tipos normais: O tipo é inferido para ser o tipo estático da expressão de troca.The first rule is that the var declaration follows the normal type inference rules: The type is inferred to be the static type of the switch expression. Com base nessa regra, o tipo sempre é correspondente.From that rule, the type always matches.

A segunda regra é que uma declaração var não tem a verificação de nulos que outras expressões do tipo padrão incluem.The second rule is that a var declaration doesn't have the null check that other type pattern expressions include. Isso significa que a variável pode ser nula e uma verificação de nulos é necessária nesse caso.That means the variable may be null, and a null check is necessary in that case.

Essas duas regras significam que, em muitos casos, uma declaração var em uma expressão case corresponde às mesmas condições que uma expressão default.Those two rules mean that in many instances, a var declaration in a case expression matches the same conditions as a default expression. Como um case não padrão é preferencial ao case default, o case default nunca será executado.Because any non-default case is preferred to the default case, the default case will never execute.

Observação

O compilador não emite um aviso nos casos em que um case default foi gravado, mas nunca será executado.The compiler does not emit a warning in those cases where a default case has been written but will never execute. Isso é coerente com a atual comportamento da instrução switch, em que todos os casos possíveis foram listados.This is consistent with current switch statement behavior where all possible cases have been listed.

A terceira regra introduz usos em que um case var pode ser útil.The third rule introduces uses where a var case may be useful. Imagine que você esteja fazendo uma correspondência de padrão em que a entrada é uma cadeia de caracteres e você está pesquisando valores de comando conhecidos.Imagine that you're doing a pattern match where the input is a string and you're searching for known command values. Você pode escrever algo parecido com:You might write something like:

static object CreateShape(string shapeDescription)
{
    switch (shapeDescription)
    {
        case "circle":
            return new Circle(2);

        case "square":
            return new Square(4);
        
        case "large-circle":
            return new Circle(12);

        case var o when (o?.Trim().Length ?? 0) == 0:
            // white space
            return null;
        default:
            return "invalid shape description";
    }            
}

O case var corresponde a null, a cadeia de caracteres vazia ou qualquer cadeia de caracteres que contém somente espaços em branco.The var case matches null, the empty string, or any string that contains only white space. Observe que o código anterior usa o operador ?. para garantir que ele não gere um NullReferenceExceptionacidentalmente.Notice that the preceding code uses the ?. operator to ensure that it doesn't accidentally throw a NullReferenceException. O case default lida com outros valores de cadeia de caracteres que não são entendidos pelo analisador de comando.The default case handles any other string values that aren't understood by this command parser.

Este é um exemplo em que talvez você queira considerar uma expressão case var diferente de uma expressão default.This is one example where you may want to consider a var case expression that is distinct from a default expression.

ConclusõesConclusions

Os constructos de correspondência de padrões permitem que você gerencie facilmente o fluxo de controle entre diferentes tipos e variáveis que não são relacionadas por uma hierarquia de herança.Pattern Matching constructs enable you to easily manage control flow among different variables and types that aren't related by an inheritance hierarchy. Você também pode controlar a lógica para usar qualquer condição testada na variável.You can also control logic to use any condition you test on the variable. Ela habilita os padrões e expressões que você precisará com mais frequência conforme cria aplicativos mais distribuídos, no qual os dados e os métodos que manipulam esses dados são separados.It enables patterns and idioms that you'll need more often as you build more distributed applications, where data and the methods that manipulate that data are separate. Você observará que os structs de forma usados nesse exemplo não contêm nenhum método, apenas propriedades somente leitura.You'll notice that the shape structs used in this sample don't contain any methods, just read-only properties. A correspondência de padrões funciona com qualquer tipo de dados.Pattern Matching works with any data type. Você escreve expressões que examinam o objeto e toma decisões de fluxo de controle com base nessas condições.You write expressions that examine the object, and make control flow decisions based on those conditions.

Compare o código deste exemplo com o design que viria após a criação de uma hierarquia de classe para um resumo de Shape e formas derivadas específicas, cada uma com sua própria implementação de um método virtual para calcular a área.Compare the code from this sample with the design that would follow from creating a class hierarchy for an abstract Shape and specific derived shapes each with their own implementation of a virtual method to calculate the area. Muitas vezes você descobrirá que as expressões de correspondência de padrões podem ser uma ferramenta muito útil quando estiver trabalhando com os dados e desejar separar as preocupações de armazenamento de dados das preocupações de comportamento.You'll often find that pattern matching expressions can be a very useful tool when you're working with data and want to separate the data storage concerns from the behavior concerns.