Padrões correspondentes: as expressões is e switch, e os operadores and, or e not em padrões

Você usa a expressão is, a instrução switch e a expressão switch para fazer a correspondência de uma expressão de entrada com qualquer número de características. O C# dá suporte a vários padrões, incluindo declaração, tipo, constante, relacional, propriedade, lista, var e descarte. Os padrões podem ser combinados usando palavras-chave da lógica booliana and, or e not.

As seguintes expressões e instruções C# dão suporte à correspondência de padrões:

Nesses constructos, você pode corresponder uma expressão de entrada com qualquer um dos seguintes padrões:

  • Padrão de declaração: para verificar o tipo de tempo de execução de uma expressão e, se uma correspondência for bem-sucedida, atribua um resultado de expressão a uma variável declarada.
  • Padrão de tipo: para verificar o tipo de tempo de execução de uma expressão.
  • Padrão constante: para testar se um resultado de expressão é igual a uma constante especificada.
  • Padrões relacionais: para comparar um resultado de expressão com uma constante especificada.
  • Padrões lógicos: para testar se uma expressão corresponde a uma combinação lógica de padrões.
  • Padrão de propriedade: para testar se as propriedades ou campos de uma expressão correspondem a padrões aninhados.
  • Padrão posicional: para desconstruir um resultado de expressão e testar se os valores resultantes correspondem a padrões aninhados.
  • var padrão: para corresponder a qualquer expressão e atribuir seu resultado a uma variável declarada.
  • Descartar padrão: para corresponder a qualquer expressão.
  • Padrões de lista: para testar se os elementos de sequência correspondem aos padrões aninhados correspondentes. Introduzido em C# 11.

Logic, property, positionale list são padrões recursivos. Ou seja, eles podem conter padrões aninhados.

Para obter o exemplo de como usar esses padrões para criar um algoritmo controlado por dados, consulte Tutorial: Usar a correspondência de padrões para criar algoritmos controlados por tipo e controlados por dados.

Padrões de declaração e tipo

Você usa padrões de declaração e tipo para verificar se o tipo de tempo de execução de uma expressão é compatível com um determinado tipo. Com um padrão de declaração, você também pode declarar uma nova variável local. Quando um padrão de declaração corresponde a uma expressão, essa variável recebe um resultado de expressão convertida, como mostra o exemplo a seguir:

object greeting = "Hello, World!";
if (greeting is string message)
{
    Console.WriteLine(message.ToLower());  // output: hello, world!
}

Um padrão de declaração com tipo T corresponde a uma expressão quando um resultado de expressão não é nulo e qualquer uma das seguintes condições é verdadeira:

  • O tipo do resultado de uma expressão em tempo de execução é T.

  • O tipo do resultado de uma expressão em tempo de execução deriva do tipo T, implementa a interface T ou existe outra conversão de referência implícita dele para T. O exemplo a seguir demonstra dois casos em que essa condição é verdadeira:

    var numbers = new int[] { 10, 20, 30 };
    Console.WriteLine(GetSourceLabel(numbers));  // output: 1
    
    var letters = new List<char> { 'a', 'b', 'c', 'd' };
    Console.WriteLine(GetSourceLabel(letters));  // output: 2
    
    static int GetSourceLabel<T>(IEnumerable<T> source) => source switch
    {
        Array array => 1,
        ICollection<T> collection => 2,
        _ => 3,
    };
    

    No exemplo anterior, na primeira chamada ao método GetSourceLabel, o primeiro padrão corresponde a um valor de argumento porque o tipo int[] de tempo de execução do argumento deriva do tipo Array. Na segunda chamada para o método GetSourceLabel, o tipo List<T> de tempo de execução do argumento não deriva do tipo Array, mas implementa a interface ICollection<T>.

  • O tipo do resultado de uma expressão em tempo de execução é um tipo de valor anulável com o tipo subjacente T.

  • Existe uma conversão boxing ou uma conversão unboxing do tipo do resultado de uma expressão em tempo de execução para o tipo T.

O exemplo a seguir demonstra as duas últimas condições:

int? xNullable = 7;
int y = 23;
object yBoxed = y;
if (xNullable is int a && yBoxed is int b)
{
    Console.WriteLine(a + b);  // output: 30
}

Se você quiser verificar apenas o tipo de uma expressão, poderá usar um descarte _ no lugar do nome de uma variável, como mostra o exemplo a seguir:

public abstract class Vehicle {}
public class Car : Vehicle {}
public class Truck : Vehicle {}

public static class TollCalculator
{
    public static decimal CalculateToll(this Vehicle vehicle) => vehicle switch
    {
        Car _ => 2.00m,
        Truck _ => 7.50m,
        null => throw new ArgumentNullException(nameof(vehicle)),
        _ => throw new ArgumentException("Unknown type of a vehicle", nameof(vehicle)),
    };
}

Para essa finalidade, você pode usar um padrão de tipo, conforme mostrado no seguinte exemplo:

public static decimal CalculateToll(this Vehicle vehicle) => vehicle switch
{
    Car => 2.00m,
    Truck => 7.50m,
    null => throw new ArgumentNullException(nameof(vehicle)),
    _ => throw new ArgumentException("Unknown type of a vehicle", nameof(vehicle)),
};

Como um padrão de declaração, um padrão de tipo corresponde a uma expressão quando um resultado de expressão não é nulo e seu tipo de tempo de execução atende a qualquer uma das condições listadas acima.

Para verificar se há não nulo, você pode usar um padrão negatednullconstant, como mostra o exemplo a seguir:

if (input is not null)
{
    // ...
}

Para obter mais informações, consulte as seções padrão declaração e padrão de tipo das notas de proposta do recurso.

Padrão de constante

Você usa um padrão constante para testar se um resultado de expressão é igual a uma constante especificada, como mostra o exemplo a seguir:

public static decimal GetGroupTicketPrice(int visitorCount) => visitorCount switch
{
    1 => 12.0m,
    2 => 20.0m,
    3 => 27.0m,
    4 => 32.0m,
    0 => 0.0m,
    _ => throw new ArgumentException($"Not supported number of visitors: {visitorCount}", nameof(visitorCount)),
};

Em um padrão constante, você pode usar qualquer expressão constante, como:

A expressão deve ser um tipo que seja conversível para o tipo constante, com uma exceção: uma expressão cujo tipo é Span<char> ou ReadOnlySpan<char> pode ser correspondida com cadeias de caracteres constantes em C# 11 e versões posteriores.

Use um padrão constante para verificar null, como mostra o exemplo a seguir:

if (input is null)
{
    return;
}

O compilador garante que nenhum operador de igualdade sobrecarregado pelo usuário == seja invocado quando a expressão x is null for avaliada.

Use um padrão de constante negadonull para verificar se ele não é nulo, conforme mostrado no seguinte exemplo:

if (input is not null)
{
    // ...
}

Para obter mais informações, consulte a seção Padrão constante da nota da proposta do recurso.

Padrões relacionais

Use um padrão relacional para comparar um resultado de expressão com uma constante, conforme mostrado no seguinte exemplo:

Console.WriteLine(Classify(13));  // output: Too high
Console.WriteLine(Classify(double.NaN));  // output: Unknown
Console.WriteLine(Classify(2.4));  // output: Acceptable

static string Classify(double measurement) => measurement switch
{
    < -4.0 => "Too low",
    > 10.0 => "Too high",
    double.NaN => "Unknown",
    _ => "Acceptable",
};

Em um padrão relacional, você pode usar qualquer um dos operadores relacionais<, >, <= ou >=. A parte direita de um padrão relacional deve ser uma expressão constante. A expressão constante pode ser de um tipo inteiro, ponto flutuante, char ou enumerado.

Para verificar se um resultado de expressão está em um determinado intervalo, corresponda-o a um padrão conjuntivoand, como mostra o exemplo a seguir:

Console.WriteLine(GetCalendarSeason(new DateTime(2021, 3, 14)));  // output: spring
Console.WriteLine(GetCalendarSeason(new DateTime(2021, 7, 19)));  // output: summer
Console.WriteLine(GetCalendarSeason(new DateTime(2021, 2, 17)));  // output: winter

static string GetCalendarSeason(DateTime date) => date.Month switch
{
    >= 3 and < 6 => "spring",
    >= 6 and < 9 => "summer",
    >= 9 and < 12 => "autumn",
    12 or (>= 1 and < 3) => "winter",
    _ => throw new ArgumentOutOfRangeException(nameof(date), $"Date with unexpected month: {date.Month}."),
};

Se um resultado de expressão for null ou não for convertido no tipo de uma constante por uma conversão anulável ou de unboxing, um padrão relacional não corresponderá a uma expressão.

Para obter mais informações, consulte a seção Padrões relacionais da nota da proposta do recurso.

Padrões lógicos

Use os combinadores de padrões not, and e or para criar os seguintes padrões lógicos:

  • Negaçãonot padrão que corresponde a uma expressão quando o padrão negado não corresponde à expressão. O exemplo a seguir mostra como você pode negar um padrão constantenull para verificar se uma expressão não é nula:

    if (input is not null)
    {
        // ...
    }
    
  • Negaçãoand padrão que corresponde a uma expressão quando o padrão negado não corresponde à expressão. O exemplo a seguir mostra como você pode combinar padrões relacionais para verificar se um valor está em um determinado intervalo:

    Console.WriteLine(Classify(13));  // output: High
    Console.WriteLine(Classify(-100));  // output: Too low
    Console.WriteLine(Classify(5.7));  // output: Acceptable
    
    static string Classify(double measurement) => measurement switch
    {
        < -40.0 => "Too low",
        >= -40.0 and < 0 => "Low",
        >= 0 and < 10.0 => "Acceptable",
        >= 10.0 and < 20.0 => "High",
        >= 20.0 => "Too high",
        double.NaN => "Unknown",
    };
    
  • Disjuntivoor padrão que corresponde a uma expressão quando um dos padrões corresponde à expressão, como mostra o exemplo a seguir:

    Console.WriteLine(GetCalendarSeason(new DateTime(2021, 1, 19)));  // output: winter
    Console.WriteLine(GetCalendarSeason(new DateTime(2021, 10, 9)));  // output: autumn
    Console.WriteLine(GetCalendarSeason(new DateTime(2021, 5, 11)));  // output: spring
    
    static string GetCalendarSeason(DateTime date) => date.Month switch
    {
        3 or 4 or 5 => "spring",
        6 or 7 or 8 => "summer",
        9 or 10 or 11 => "autumn",
        12 or 1 or 2 => "winter",
        _ => throw new ArgumentOutOfRangeException(nameof(date), $"Date with unexpected month: {date.Month}."),
    };
    

Como mostra o exemplo anterior, você pode usar repetidamente os combinadores de padrão em um padrão.

Precedência e ordem da avaliação

Os combinadores de padrão são ordenados da precedência mais alta para a mais baixa da seguinte maneira:

  • not
  • and
  • or

Quando um padrão lógico for um padrão de uma is expressão, a precedência de combinadores de padrões lógicos é maior do que a precedência dos operadores lógicos (operadores lógicos bit a bit e boolianos lógicos). Caso contrário, a precedência dos combinadores de padrões lógicos é menor do que a precedência de operadores lógicos e lógicos condicionais. Para obter a lista completa de operadores C# ordenados por nível de precedência, consulte a seção Precedência de operador do artigo Operadores C# .

Para especificar explicitamente a precedência, use parênteses, como mostra o exemplo a seguir:

static bool IsLetter(char c) => c is (>= 'a' and <= 'z') or (>= 'A' and <= 'Z');

Observação

A ordem na qual os padrões são verificados é indefinida. Em tempo de execução, os padrões aninhados à direita dos padrões or e and podem ser verificados primeiro.

Para obter mais informações, consulte a seção Padrão constante da nota da proposta do recurso.

Padrão de propriedade

Você usa um padrão de propriedade para corresponder às propriedades ou campos de uma expressão com padrões aninhados, como mostra o exemplo a seguir:

static bool IsConferenceDay(DateTime date) => date is { Year: 2020, Month: 5, Day: 19 or 20 or 21 };

Um padrão de propriedade corresponde a uma expressão quando um resultado de expressão não é nulo e cada padrão aninhado corresponde à propriedade ou campo correspondente do resultado da expressão.

Você também pode adicionar uma verificação de tipo em tempo de execução e uma declaração de variável a um padrão de propriedade, como mostra o exemplo a seguir:

Console.WriteLine(TakeFive("Hello, world!"));  // output: Hello
Console.WriteLine(TakeFive("Hi!"));  // output: Hi!
Console.WriteLine(TakeFive(new[] { '1', '2', '3', '4', '5', '6', '7' }));  // output: 12345
Console.WriteLine(TakeFive(new[] { 'a', 'b', 'c' }));  // output: abc

static string TakeFive(object input) => input switch
{
    string { Length: >= 5 } s => s.Substring(0, 5),
    string s => s,

    ICollection<char> { Count: >= 5 } symbols => new string(symbols.Take(5).ToArray()),
    ICollection<char> symbols => new string(symbols.ToArray()),

    null => throw new ArgumentNullException(nameof(input)),
    _ => throw new ArgumentException("Not supported input type."),
};

Um padrão de propriedade é um padrão recursivo. Ou seja, você pode usar qualquer padrão como aninhado. Use um padrão de propriedade para corresponder partes de dados com padrões aninhados, como mostra o exemplo a seguir:

public record Point(int X, int Y);
public record Segment(Point Start, Point End);

static bool IsAnyEndOnXAxis(Segment segment) =>
    segment is { Start: { Y: 0 } } or { End: { Y: 0 } };

O exemplo anterior usa o orcombinador de padrões e os tipos de registro.

A partir do C# 10, você pode referenciar propriedades aninhadas ou campos dentro de um padrão de propriedade. Essa capacidade é conhecida como um padrão de propriedade estendido. Por exemplo, você pode refatorar o método do exemplo anterior para o seguinte código equivalente:

static bool IsAnyEndOnXAxis(Segment segment) =>
    segment is { Start.Y: 0 } or { End.Y: 0 };

Para obter mais informações, consulte a seção Padrão de propriedade da nota de proposta do recurso e a nota de proposta de recurso Padrões de propriedade estendida.

Dica

Você pode usar a regra de estilo Simplificar padrão de propriedade (IDE0170) para melhorar a legibilidade do código sugerindo locais para usar padrões de propriedade estendidos.

Padrão posicional

Você usa um padrão posicional para desconstruir um resultado de expressão e corresponder aos valores resultantes com os padrões aninhados correspondentes, como mostra o exemplo a seguir:

public readonly struct Point
{
    public int X { get; }
    public int Y { get; }

    public Point(int x, int y) => (X, Y) = (x, y);

    public void Deconstruct(out int x, out int y) => (x, y) = (X, Y);
}

static string Classify(Point point) => point switch
{
    (0, 0) => "Origin",
    (1, 0) => "positive X basis end",
    (0, 1) => "positive Y basis end",
    _ => "Just a point",
};

No exemplo anterior, o tipo de expressão contém o método Deconstrução, que é usado para desconstruir um resultado de expressão. Você também pode corresponder expressões de tipos de tupla com padrões posicionais. Dessa forma, você pode corresponder várias entradas com vários padrões, como mostra o exemplo a seguir:

static decimal GetGroupTicketPriceDiscount(int groupSize, DateTime visitDate)
    => (groupSize, visitDate.DayOfWeek) switch
    {
        (<= 0, _) => throw new ArgumentException("Group size must be positive."),
        (_, DayOfWeek.Saturday or DayOfWeek.Sunday) => 0.0m,
        (>= 5 and < 10, DayOfWeek.Monday) => 20.0m,
        (>= 10, DayOfWeek.Monday) => 30.0m,
        (>= 5 and < 10, _) => 12.0m,
        (>= 10, _) => 15.0m,
        _ => 0.0m,
    };

O exemplo anterior usa padrões relacionais e lógicos.

Você pode usar os nomes de elementos de tupla e parâmetros Deconstruct em um padrão posicional, como mostra o exemplo a seguir:

var numbers = new List<int> { 1, 2, 3 };
if (SumAndCount(numbers) is (Sum: var sum, Count: > 0))
{
    Console.WriteLine($"Sum of [{string.Join(" ", numbers)}] is {sum}");  // output: Sum of [1 2 3] is 6
}

static (double Sum, int Count) SumAndCount(IEnumerable<int> numbers)
{
    int sum = 0;
    int count = 0;
    foreach (int number in numbers)
    {
        sum += number;
        count++;
    }
    return (sum, count);
}

Você também pode estender um padrão posicional de qualquer uma das seguintes maneiras:

  • Adicione uma verificação de tipo de tempo de execução e uma declaração de variável, como mostra o exemplo a seguir:

    public record Point2D(int X, int Y);
    public record Point3D(int X, int Y, int Z);
    
    static string PrintIfAllCoordinatesArePositive(object point) => point switch
    {
        Point2D (> 0, > 0) p => p.ToString(),
        Point3D (> 0, > 0, > 0) p => p.ToString(),
        _ => string.Empty,
    };
    

    O exemplo anterior usa registros posicionais que fornecem implicitamente o método Deconstruct.

  • Use um padrão de propriedade dentro de um padrão posicional, como mostra o exemplo a seguir:

    public record WeightedPoint(int X, int Y)
    {
        public double Weight { get; set; }
    }
    
    static bool IsInDomain(WeightedPoint point) => point is (>= 0, >= 0) { Weight: >= 0.0 };
    
  • Combine dois usos anteriores, como mostra o exemplo a seguir:

    if (input is WeightedPoint (> 0, > 0) { Weight: > 0.0 } p)
    {
        // ..
    }
    

Um padrão posicional é um padrão recursivo. Ou seja, você pode usar qualquer padrão como aninhado.

Para obter mais informações, consulte a seção Padrão constante da nota da proposta do recurso.

Padrão var

Você usa um var padrão para corresponder a qualquer expressão, incluindo null, e atribuir seu resultado a uma nova variável local, como mostra o exemplo a seguir:

static bool IsAcceptable(int id, int absLimit) =>
    SimulateDataFetch(id) is var results 
    && results.Min() >= -absLimit 
    && results.Max() <= absLimit;

static int[] SimulateDataFetch(int id)
{
    var rand = new Random();
    return Enumerable
               .Range(start: 0, count: 5)
               .Select(s => rand.Next(minValue: -10, maxValue: 11))
               .ToArray();
}

Um padrão var é útil quando você precisa de uma variável temporária dentro de uma expressão booliana para manter o resultado de cálculos intermediários. Você também poderá usar um padrão var quando precisar realizar mais verificações em proteções de caso when de uma expressão ou instrução switch, como mostra o exemplo a seguir:

public record Point(int X, int Y);

static Point Transform(Point point) => point switch
{
    var (x, y) when x < y => new Point(-x, y),
    var (x, y) when x > y => new Point(x, -y),
    var (x, y) => new Point(x, y),
};

static void TestTransform()
{
    Console.WriteLine(Transform(new Point(1, 2)));  // output: Point { X = -1, Y = 2 }
    Console.WriteLine(Transform(new Point(5, 2)));  // output: Point { X = 5, Y = -2 }
}

No exemplo anterior, o padrão var (x, y) é equivalente a um padrão posicional(var x, var y).

Em um padrão var, o tipo de uma variável declarada é o tipo de tempo de compilação da expressão que corresponde ao padrão.

Para obter mais informações, consulte a seção Padrão constante da nota da proposta do recurso.

Padrão de descarte

Você usa um padrão de descarte_ para corresponder a qualquer expressão, incluindo null, conforme mostra o exemplo a seguir:

Console.WriteLine(GetDiscountInPercent(DayOfWeek.Friday));  // output: 5.0
Console.WriteLine(GetDiscountInPercent(null));  // output: 0.0
Console.WriteLine(GetDiscountInPercent((DayOfWeek)10));  // output: 0.0

static decimal GetDiscountInPercent(DayOfWeek? dayOfWeek) => dayOfWeek switch
{
    DayOfWeek.Monday => 0.5m,
    DayOfWeek.Tuesday => 12.5m,
    DayOfWeek.Wednesday => 7.5m,
    DayOfWeek.Thursday => 12.5m,
    DayOfWeek.Friday => 5.0m,
    DayOfWeek.Saturday => 2.5m,
    DayOfWeek.Sunday => 2.0m,
    _ => 0.0m,
};

Um nullpadrão de descarteDayOfWeek: para manipular qualquer valor inteiro que não tenha o membro correspondente da enumeração (por exemplo, ). Isso garante que uma expressão switch no exemplo manipule todos os valores de entrada possíveis. Se você não usar um padrão de descarte em uma expressão switch e nenhum dos padrões da expressão corresponder a uma entrada, o runtime gerará uma exceção. O compilador gera um aviso se uma expressão switch não manipular todos os valores de entrada possíveis.

Um padrão de descarte não pode ser um padrão em uma expressão is ou uma instrução switch. Nesses casos, para corresponder a qualquer expressão, use um var padrão com um descarte: var _. Um padrão de descarte não pode ser um padrão em uma switch expressão.

Para obter mais informações, consulte a seção Padrão constante da nota da proposta do recurso.

Padrão entre parênteses

Você pode colocar parênteses em torno de qualquer padrão. Normalmente, você faz isso para enfatizar ou alterar a precedência em padrões lógicos, como mostra o exemplo a seguir:

if (input is not (float or double))
{
    return;
}

Padrões de lista

A partir do C# 11, você pode fazer a correspondência de uma matriz ou de uma lista com uma sequência como mostra o exemplo a seguir:

int[] numbers = { 1, 2, 3 };

Console.WriteLine(numbers is [1, 2, 3]);  // True
Console.WriteLine(numbers is [1, 2, 4]);  // False
Console.WriteLine(numbers is [1, 2, 3, 4]);  // False
Console.WriteLine(numbers is [0 or 1, <= 2, >= 3]);  // True

Como mostra o exemplo anterior, um padrão de lista é correspondido quando cada padrão aninhado é correspondido pelo elemento correspondente de uma sequência de entrada. Você pode usar qualquer padrão dentro de um padrão de lista. Para corresponder a qualquer elemento, use o padrão discard ou, se você também quiser capturar o elemento, o padrão var, como mostra o exemplo a seguir:

List<int> numbers = new() { 1, 2, 3 };

if (numbers is [var first, _, _])
{
    Console.WriteLine($"The first element of a three-item list is {first}.");
}
// Output:
// The first element of a three-item list is 1.

Os exemplos anteriores correspondem a uma sequência de entrada inteira em relação a um padrão de lista. Para corresponder elementos somente no início ou/e no final de uma sequência de entrada, use o padrão de fatia.. como mostra o seguinte exemplo:

Console.WriteLine(new[] { 1, 2, 3, 4, 5 } is [> 0, > 0, ..]);  // True
Console.WriteLine(new[] { 1, 1 } is [_, _, ..]);  // True
Console.WriteLine(new[] { 0, 1, 2, 3, 4 } is [> 0, > 0, ..]);  // False
Console.WriteLine(new[] { 1 } is [1, 2, ..]);  // False

Console.WriteLine(new[] { 1, 2, 3, 4 } is [.., > 0, > 0]);  // True
Console.WriteLine(new[] { 2, 4 } is [.., > 0, 2, 4]);  // False
Console.WriteLine(new[] { 2, 4 } is [.., 2, 4]);  // True

Console.WriteLine(new[] { 1, 2, 3, 4 } is [>= 0, .., 2 or 4]);  // True
Console.WriteLine(new[] { 1, 0, 0, 1 } is [1, 0, .., 0, 1]);  // True
Console.WriteLine(new[] { 1, 0, 1 } is [1, 0, .., 0, 1]);  // False

Um padrão de fatia corresponde a zero ou mais elementos. Você pode usar no máximo um padrão de fatia em um padrão de lista. O padrão de fatia só pode aparecer em um padrão de lista.

Você também pode aninhar um subpadrão dentro de um padrão de fatia, como mostra o exemplo a seguir:

void MatchMessage(string message)
{
    var result = message is ['a' or 'A', .. var s, 'a' or 'A']
        ? $"Message {message} matches; inner part is {s}."
        : $"Message {message} doesn't match.";
    Console.WriteLine(result);
}

MatchMessage("aBBA");  // output: Message aBBA matches; inner part is BB.
MatchMessage("apron");  // output: Message apron doesn't match.

void Validate(int[] numbers)
{
    var result = numbers is [< 0, .. { Length: 2 or 4 }, > 0] ? "valid" : "not valid";
    Console.WriteLine(result);
}

Validate(new[] { -1, 0, 1 });  // output: not valid
Validate(new[] { -1, 0, 0, 1 });  // output: valid

Para obter mais informações, confira a nota de proposta de recursos de Padrões de lista.

Especificação da linguagem C#

Para obter mais informações, confira a seção Padrões e padrões correspondentes da especificação da linguagem C#.

Para informações sobre os recursos adicionados ao C# 8 e versões posteriores, confira as seguintes notas sobre a proposta de recursos:

Confira também