Correspondência de padrões em C # 7

Finalmente, o C # 7 tem um recurso aguardado chamado Correspondência de Padrões. Se você estiver familiarizado com linguagens funcionais, como F #, essa função na forma em que ela existe atualmente pode decepcioná-lo um pouco. Mas ainda hoje, ele pode simplificar o código em vários casos. Mais detalhes sob o corte!



Cada novo recurso pode ser perigoso para o desenvolvedor, criando o aplicativo para o qual o desempenho é crítico. Novos níveis de abstração são bons, mas para usá-los de maneira eficaz, é necessário entender como eles realmente funcionam. Este artigo discute a função de correspondência de padrões e como ela funciona.

Uma amostra em C # pode ser usada em uma expressão é, bem como no bloco de caso de uma instrução switch.
Existem três tipos de amostras:

  • constante de amostra;
  • tipo de amostra;
  • variável de amostra.

A correspondência de padrões em é expressões


public void IsExpressions(object o) { // Alternative way checking for null if (o is null) Console.WriteLine("o is null"); // Const pattern can refer to a constant value const double value = double.NaN; if (o is value) Console.WriteLine("o is value"); // Const pattern can use a string literal if (o is "o") Console.WriteLine("o is \"o\""); // Type pattern if (o is int n) Console.WriteLine(n); // Type pattern and compound expressions if (o is string s && s.Trim() != string.Empty) Console.WriteLine("o is not blank"); } 

Usando a expressão is, é possível verificar se o valor é constante e, usando a verificação de tipo, é possível determinar adicionalmente a variável de amostra.

Ao usar a correspondência de padrões nas expressões is, você deve prestar atenção a vários pontos interessantes:

  • A variável inserida pela instrução if é enviada para o escopo externo.
  • A variável inserida pela instrução if é explicitamente designada apenas quando o padrão corresponde.
  • A implementação atual da correspondência de padrões nas expressões não é muito eficiente.

Primeiro, considere os dois primeiros casos:

 public void ScopeAndDefiniteAssigning(object o) { if (o is string s && s.Length != 0) { Console.WriteLine("o is not empty string"); } // Can't use 's' any more. 's' is already declared in the current scope. if (o is int n || (o is string s2 && int.TryParse(s2, out n))) { Console.WriteLine(n); } } 

A primeira instrução if apresenta a variável s, visível dentro de todo o método. Isso é razoável, mas complica a lógica se outras instruções if no mesmo bloco tentarem reutilizar o mesmo nome. Nesse caso, certifique-se de usar um nome diferente para evitar conflitos.

A variável inserida na expressão is é atribuída explicitamente apenas quando o predicado é verdadeiro. Isso significa que a variável n na segunda instrução if não está atribuída no operando certo, mas como já está declarada, podemos usá-la como a variável out no método int.TryParse.

O terceiro ponto mencionado acima é o mais importante. Considere o seguinte exemplo:

 public void BoxTwice(int n) { if (n is 42) Console.WriteLine("n is 42"); } 

Na maioria dos casos, a expressão é convertida em object.Equals (constante, variável) [embora as características digam que o operador == deve ser usado para tipos simples]:

 public void BoxTwice(int n) { if (object.Equals(42, n)) { Console.WriteLine("n is 42"); } } 

Esse código chama dois processos de conversão de empacotamento que podem afetar significativamente o desempenho se usados ​​em um caminho crítico do aplicativo. Anteriormente, a expressão o é nula chamada de empacotamento, se a variável o era de um tipo anulável (consulte Código subótimo para e é nulo ), mas há esperança de que isso seja corrigido (aqui está a solicitação correspondente no github ).

Se a variável n for do tipo objeto, a expressão o é 42 causará um processo de "conversão de embalagem" (para o literal 42), embora um código semelhante baseado na instrução switch não leve a isso.

Variável de amostra em is expression


Um padrão variável é um tipo especial de tipo de padrão com uma grande diferença: o padrão corresponderá a qualquer valor, mesmo nulo.

 public void IsVar(object o) { if (o is var x) Console.WriteLine($"x: {x}"); } 

A expressão o is object será verdadeira se o não for nulo, mas a expressão o for var x será sempre verdadeira. Portanto, o compilador no modo de liberação * exclui completamente as instruções if e simplesmente sai da chamada do método Console. Infelizmente, o compilador não avisa sobre a indisponibilidade do código no seguinte caso: if (! (O é var x)) Console.WriteLine ("Inacessível"). Há esperança de que isso também seja corrigido.

* Não está claro por que o comportamento difere apenas no modo de liberação. Parece que a raiz de todos os problemas é a mesma: a implementação inicial da função não é ideal. No entanto, a julgar por esse comentário de Neal Gafter, tudo mudará em breve: “O código para correspondência com a amostra será reescrito do zero (para também suportar amostras recursivas). Eu acho que a maioria das melhorias que você está falando será implementada no novo código e disponível gratuitamente. No entanto, isso levará algum tempo. ”

A ausência de uma verificação nula torna essa situação especial e potencialmente perigosa. No entanto, se você souber exatamente como esse exemplo funciona, poderá ser útil. Pode ser usado para introduzir uma variável temporária na expressão:

 public void VarPattern(IEnumerable<string> s) { if (s.FirstOrDefault(o => o != null) is var v && int.TryParse(v, out var n)) { Console.WriteLine(n); } } 

É expressão e declaração de Elvis


Há outro caso que pode ser útil. Um tipo de amostra corresponde a um valor somente quando não é nulo. Podemos usar essa lógica de "filtragem" com um operador de distribuição nula para tornar o código mais legível:

 public void WithNullPropagation(IEnumerable<string> s) { if (s?.FirstOrDefault(str => str.Length > 10)?.Length is int length) { Console.WriteLine(length); } // Similar to if (s?.FirstOrDefault(str => str.Length > 10)?.Length is var length2 && length2 != null) { Console.WriteLine(length2); } // And similar to var length3 = s?.FirstOrDefault(str => str.Length > 10)?.Length; if (length3 != null) { Console.WriteLine(length3); } } 

Observe que o mesmo padrão pode ser usado para os tipos de valor e de referência.

Correspondência de padrões em blocos de caso


A funcionalidade da instrução switch foi estendida no C # 7, para que agora os padrões possam ser usados ​​nas cláusulas case:

 public static int Count<T>(this IEnumerable<T> e) { switch (e) { case ICollection<T> c: return c.Count; case IReadOnlyCollection<T> c: return c.Count; // Matches concurrent collections case IProducerConsumerCollection<T> pc: return pc.Count; // Matches if e is not null case IEnumerable<T> _: return e.Count(); // Default case is handled when e is null default: return 0; } } 

Este exemplo mostra o primeiro conjunto de alterações na instrução switch.

  1. Uma variável de qualquer tipo pode ser usada com a instrução switch.
  2. A cláusula case permite especificar um padrão.
  3. A ordem das cláusulas do caso é importante. O compilador lançará um erro se a sentença anterior corresponder ao tipo base e a próxima à derivada.
  4. As ofertas personalizadas são implicitamente verificadas como nulas **. No exemplo acima, a última cláusula case é válida porque corresponde apenas quando o argumento não é nulo.

** A última frase do caso mostra outra função adicionada no C # 7 - amostras de uma variável vazia. O nome especial _ informa ao compilador que a variável não é necessária. O exemplo de tipo na cláusula case requer um alias. Mas se você não precisar, pode usar _.

O fragmento a seguir mostra outro recurso da correspondência de padrões com base na instrução switch - a capacidade de usar predicados:

 public static void FizzBuzz(object o) { switch (o) { case string s when s.Contains("Fizz") || s.Contains("Buzz"): Console.WriteLine(s); break; case int n when n % 5 == 0 && n % 3 == 0: Console.WriteLine("FizzBuzz"); break; case int n when n % 5 == 0: Console.WriteLine("Fizz"); break; case int n when n % 3 == 0: Console.WriteLine("Buzz"); break; case int n: Console.WriteLine(n); break; } } 

Esta é uma versão estranha da tarefa do FizzBuzz que processa um objeto, não apenas um número.

Uma instrução switch pode incluir várias cláusulas de caso do mesmo tipo. Nesse caso, o compilador combina todas as verificações de tipo para evitar cálculos desnecessários:

 public static void FizzBuzz(object o) { // All cases can match only if the value is not null if (o != null) { if (o is string s && (s.Contains("Fizz") || s.Contains("Buzz"))) { Console.WriteLine(s); return; } bool isInt = o is int; int num = isInt ? ((int)o) : 0; if (isInt) { // The type check and unboxing happens only once per group if (num % 5 == 0 && num % 3 == 0) { Console.WriteLine("FizzBuzz"); return; } if (num % 5 == 0) { Console.WriteLine("Fizz"); return; } if (num % 3 == 0) { Console.WriteLine("Buzz"); return; } Console.WriteLine(num); } } } 

Mas há duas coisas a ter em mente:

1. O compilador combina apenas verificações seqüenciais de tipo e, se você misturar cláusulas de maiúsculas e minúsculas com tipos diferentes, um código de qualidade inferior será gerado:

 switch (o) { // The generated code is less optimal: // If o is int, then more than one type check and unboxing operation // may happen. case int n when n == 1: return 1; case string s when s == "": return 2; case int n when n == 2: return 3; default: return -1; } 

O compilador irá convertê-lo da seguinte maneira:

if (o é int n && n == 1) retorna 1;
 if (o is string s && s == "") return 2; if (o is int n2 && n2 == 2) return 3; return -1; 

2. O compilador faz todo o possível para evitar problemas típicos de seqüenciamento.

 switch (o) { case int n: return 1; // Error: The switch case has already been handled by a previous case. case int n when n == 1: return 2; } 

No entanto, o compilador não pode determinar se um predicado é mais forte que outro e substitui efetivamente as seguintes cláusulas de caso:

 switch (o) { case int n when n > 0: return 1; // Will never match, but the compiler won't warn you about it case int n when n > 1: return 2; } 

Resumo de correspondência de padrões


  • Os seguintes padrões apareceram no C # 7: um padrão constante, um padrão de tipo, um padrão variável e um padrão variável vazio.
  • As amostras podem ser usadas em expressões e em blocos de caso.
  • A implementação do padrão constante na expressão é para tipos de valor longe do ideal em termos de desempenho.
  • As amostras de uma variável sempre correspondem; é preciso ter cuidado com elas.
  • A instrução switch pode ser usada para definir verificações de tipo com predicados adicionais nas cláusulas when.

Evento Unity em Moscou - Unity Moscow Meetup 2018.1


Na quinta-feira, 11 de outubro, o Unity Moscow Meetup 2018.1 será realizado na Escola Superior de Economia. Esta é a primeira reunião de desenvolvedores do Unity em Moscou nesta temporada. O tema do primeiro mitap será AR / VR. Você encontrará relatórios interessantes, comunicação com profissionais do setor e uma zona de demonstração especial da MSI.

Detalhes

Source: https://habr.com/ru/post/pt423229/


All Articles