Faça mais com padrões no C # 8.0

O Visual Studio 2019 Preview 2 já está disponível! E com ele, mais alguns recursos do C # 8.0 estão prontos para você experimentar. É principalmente sobre a correspondência de padrões, embora eu toque em algumas outras notícias e mudanças no final.


Original no Blog

Mais padrões em mais lugares


Quando o C # 7.0 introduziu a correspondência de padrões, dissemos que esperávamos adicionar mais padrões em mais lugares no futuro. Chegou a hora! Estamos adicionando o que chamamos de padrões recursivos , bem como uma forma de expressão mais compacta de instruções de switch chamada (você adivinhou!) Expressões de chave.


Aqui está um exemplo simples de padrões em C # 7.0 para começar:


 class 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 Display(object o) { switch (o) { case Point p when pX == 0 && pY == 0: return "origin"; case Point p: return $"({pX}, {pY})"; default: return "unknown"; } } 

Alternar expressões


Primeiro, vamos observar que muitas instruções de switch realmente não fazem muito trabalho interessante dentro dos corpos do case . Muitas vezes, todos eles apenas produzem um valor, atribuindo-o a uma variável ou retornando-o (como acima). Em todas essas situações, a instrução switch é francamente bastante desajeitada. Parece que o recurso de linguagem de cinco décadas é, com muita cerimônia.


Decidimos que era hora de adicionar uma forma de expressão de switch . Aqui está, aplicado ao exemplo acima:


 static string Display(object o) { return o switch { Point p when pX == 0 && pY == 0 => "origin", Point p => $"({pX}, {pY})", _ => "unknown" }; } 

Há várias coisas aqui que foram alteradas das instruções do switch. Vamos listá-los:


  • A palavra-chave switch é "infix" entre o valor testado e a lista de casos {...} . Isso o torna mais composicional com outras expressões e também mais fácil diferenciar visualmente de uma instrução switch.
  • A palavra-chave case e o : foram substituídas por uma seta lambda => por questões de brevidade.
  • default foi substituído pelo padrão _ descarte por questões de concisão.
  • Os corpos são expressões! O resultado do corpo selecionado se torna o resultado da expressão do comutador.

Como uma expressão precisa ter um valor ou gerar uma exceção, uma expressão de opção que chega ao fim sem uma correspondência gera uma exceção. O compilador faz um ótimo trabalho em alertá-lo quando esse for o caso, mas não o forçará a encerrar todas as expressões de chave com um clichê: você pode conhecer melhor!


Obviamente, como nosso método Display agora consiste em uma única declaração de retorno, podemos simplificá-lo para ter um corpo de expressão:


  static string Display(object o) => o switch { Point p when pX == 0 && pY == 0 => "origin", Point p => $"({pX}, {pY})", _ => "unknown" }; 

Para ser sincero, não sei ao certo qual orientação de formatação forneceremos aqui, mas deve ficar claro que isso é muito mais claro e mais claro, especialmente porque a brevidade normalmente permite que você formate a opção de maneira "tabular", como acima , com padrões e corpos na mesma linha e os => s alinhados um sob o outro.


A propósito, planejamos permitir uma vírgula à direita , após o último caso, de acordo com todas as outras "listas separadas por vírgula entre chaves" em C #, mas a Visualização 2 ainda não permite isso.


Padrões de propriedade


Falando em brevidade, os padrões estão subitamente se tornando os elementos mais pesados ​​da expressão de switch acima! Vamos fazer algo sobre isso.


Observe que a expressão switch usa o padrão de tipo Point p (duas vezes), bem como uma cláusula when para adicionar condições adicionais ao primeiro case .


No C # 8.0, estamos adicionando mais elementos opcionais ao padrão de tipo, o que permite ao próprio padrão se aprofundar no valor que está sendo correspondido. Você pode torná-lo um padrão de propriedade adicionando {...} 's contendo padrões aninhados para aplicar às propriedades ou campos acessíveis do valor. Vamos reescrever a expressão switch da seguinte maneira:


 static string Display(object o) => o switch { Point { X: 0, Y: 0 } p => "origin", Point { X: var x, Y: var y } p => $"({x}, {y})", _ => "unknown" }; 

Ambos os casos ainda verificam se o é um Point . O primeiro caso aplica o padrão constante 0 recursivamente às propriedades X e Y de p , verificando se elas têm esse valor. Assim, podemos eliminar a cláusula when neste e em muitos casos comuns.


O segundo caso aplica o padrão var a cada um de X e Y Lembre-se de que o padrão var no C # 7.0 sempre é bem-sucedido e simplesmente declara uma variável nova para manter o valor. Assim, y contêm os valores int de pX e pY .


Nós nunca usamos p , e podemos omitir aqui:


  Point { X: 0, Y: 0 } => "origin", Point { X: var x, Y: var y } => $"({x}, {y})", _ => "unknown" 

Uma coisa que permanece verdadeira para todos os padrões de tipo, incluindo padrões de propriedade, é que eles exigem que o valor seja não nulo. Isso abre a possibilidade de o padrão de propriedade "vazio" {} ser usado como um padrão compacto "não nulo". Por exemplo, poderíamos substituir o caso de fallback pelos dois casos a seguir:


  {} => o.ToString(), null => "null" 

O {} lida com os objetos não nulos restantes e null obtém os nulos; portanto, a opção é exaustiva e o compilador não se queixará dos valores que estão caindo.


Padrões posicionais


O padrão de propriedade não reduziu exatamente o segundo caso de Point e não parece valer a pena, mas há mais que pode ser feito.


Observe que a classe Point possui um método Deconstruct , o chamado desconstrutor . No C # 7.0, os desconstrutores permitiam que um valor fosse desconstruído na atribuição, para que você pudesse escrever, por exemplo:


 (int x, int y) = GetPoint(); // split up the Point according to its deconstructor 

O C # 7.0 não integrou a desconstrução aos padrões. Isso muda com os padrões posicionais, que são uma maneira adicional de estendermos os padrões de tipo no C # 8.0. Se o tipo correspondente for do tipo tupla ou tiver um desconstrutor, podemos usar padrões posicionais como uma maneira compacta de aplicar padrões recursivos sem precisar nomear propriedades:


 static string Display(object o) => o switch { Point(0, 0) => "origin", Point(var x, var y) => $"({x}, {y})", _ => "unknown" }; 

Depois que o objeto é correspondido como um Point , o desconstrutor é aplicado e os padrões aninhados são aplicados aos valores resultantes.


Desconstrutores nem sempre são adequados. Eles devem ser adicionados apenas aos tipos em que é realmente claro qual dos valores é qual. Para uma classe Point , por exemplo, é seguro e intuitivo assumir que o primeiro valor é X e o segundo é Y , portanto, a expressão da opção acima é intuitiva e fácil de ler.


Padrões de tupla


Um caso especial muito útil de padrões posicionais é quando eles são aplicados a tuplas. Se uma instrução switch é aplicada diretamente a uma expressão de tupla, até permitimos que o conjunto extra de parênteses seja omitido, como no switch (x, y, z) vez do switch ((x, y, z)) .


Os padrões de tupla são ótimos para testar várias partes de entrada ao mesmo tempo. Aqui está uma implementação simples de uma máquina de estado:


 static State ChangeState(State current, Transition transition, bool hasKey) => (current, transition) switch { (Opened, Close) => Closed, (Closed, Open) => Opened, (Closed, Lock) when hasKey => Locked, (Locked, Unlock) when hasKey => Closed, _ => throw new InvalidOperationException($"Invalid transition") }; 

É claro que poderíamos optar por incluir hasKey na tupla ativada em vez de usar as cláusulas when - é realmente uma questão de gosto:


 static State ChangeState(State current, Transition transition, bool hasKey) => (current, transition, hasKey) switch { (Opened, Close, _) => Closed, (Closed, Open, _) => Opened, (Closed, Lock, true) => Locked, (Locked, Unlock, true) => Closed, _ => throw new InvalidOperationException($"Invalid transition") }; 

Em suma, espero que você possa ver que padrões recursivos e expressões de chave podem levar a uma lógica de programa mais clara e declarativa.


Outros recursos do C # 8.0 na Visualização 2


Embora os recursos de padrão sejam os principais disponíveis on-line no VS 2019 Preview 2, existem alguns menores que espero que você também ache útil e divertido. Não vou entrar em detalhes aqui, mas apenas dê uma breve descrição de cada um.


Usando declarações


No C #, o using instruções sempre causa um nível de aninhamento, que pode ser altamente irritante e prejudicar a legibilidade. Nos casos simples em que você deseja que um recurso seja limpo no final de um escopo, agora você usa declarações . As declarações using são simplesmente declarações de variáveis ​​locais com uma palavra-chave using na frente, e seu conteúdo é descartado no final do bloco de instruções atual. Então, em vez de:


 static void Main(string[] args) { using (var options = Parse(args)) { if (options["verbose"]) { WriteLine("Logging..."); } ... } // options disposed here } 

Você pode simplesmente escrever


 static void Main(string[] args) { using var options = Parse(args); if (options["verbose"]) { WriteLine("Logging..."); } } // options disposed here 

Estruturas descartáveis ​​ref


As estruturas de referência foram introduzidas no C # 7.2, e este não é o lugar para reiterar sua utilidade, mas, em troca, elas apresentam algumas limitações severas, como não poder implementar interfaces. Agora, as estruturas de referência podem ser descartadas sem a implementação da interface IDisposable , simplesmente com um método Dispose .


Funções locais estáticas


Se você deseja garantir que sua função local não incorra nos custos de tempo de execução associados à "captura" (referência) de variáveis ​​do escopo anexo, você pode declarar como static . Então o compilador impedirá a referência de qualquer coisa declarada nas funções anexas - exceto outras funções locais estáticas!


Alterações desde a visualização 1


Os principais recursos da Visualização 1 eram tipos de referência anuláveis ​​e fluxos assíncronos. Ambos evoluíram um pouco na Visualização 2, portanto, se você começou a usá-los, é bom estar ciente a seguir.


Tipos de referência anuláveis


Adicionamos mais opções para controlar avisos anuláveis ​​na fonte (por meio #nullable diretivas de #pragma warning #nullable e #pragma warning ) e no nível do projeto. Também alteramos a inclusão do arquivo do projeto para <NullableContextOptions>enable</NullableContextOptions> .


Fluxos assíncronos


Mudamos o formato da interface IAsyncEnumerable<T> o compilador espera! Isso deixa o compilador fora de sincronia com a interface fornecida no .NET Core 3.0 Preview 1, o que pode causar alguns problemas. No entanto, o .NET Core 3.0 Preview 2 será lançado em breve e isso trará as interfaces novamente em sincronia.


Tenha nisso!


Como sempre, estamos ansiosos pelo seu feedback! Por favor, brinque com os novos recursos de padrão em particular. Você corre em paredes de tijolo? É algo chato? Quais são alguns cenários interessantes e úteis que você encontra para eles? Aperte o botão de feedback e informe-nos!


Happy hacking


Mads Torgersen, líder de projeto para C #

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


All Articles