Nota
O acesso a esta página requer autorização. Pode tentar iniciar sessão ou alterar os diretórios.
O acesso a esta página requer autorização. Pode tentar alterar os diretórios.
A correspondência de padrões permite-te adicionar funcionalidades para tipos definidos noutras bibliotecas sem modificar esses tipos. Outro uso para padrões é criar funcionalidades que seu aplicativo requer que não sejam um recurso fundamental do tipo que está sendo estendido.
Neste tutorial, aprenderás como:
- Reconhecer situações em que a correspondência de padrões deve ser usada.
- Use expressões de correspondência de padrões para implementar o comportamento com base em tipos e valores de propriedade.
- Combine a correspondência de padrões com outras técnicas para criar algoritmos completos.
Prerequisites
- O SDK mais recente .NET
- Editor Visual Studio Code
- O Kit de Desenvolvimento C#
Instruções de instalação
No Windows, utilize este ficheiro de configuração WinGet para instalar todos os pré-requisitos. Se você já tiver algo instalado, o WinGet ignorará essa etapa.
- Baixe o arquivo e clique duas vezes para executá-lo.
- Leia o contrato de licença, digite ye selecione Enter quando solicitado a aceitar.
- Se surgir um aviso de Controle de Conta de Utilizador (UAC) intermitente na barra de tarefas, permita que a instalação prossiga.
Em outras plataformas, você precisa instalar cada um desses componentes separadamente.
- Baixe o instalador recomendado na página de download do .NET SDK e clique duas vezes para executá-lo. A página de download deteta sua plataforma e recomenda o instalador mais recente para sua plataforma.
- Descarregue o instalador mais recente na página inicial Visual Studio Code e clique duas vezes para o executar. Essa página também deteta a sua plataforma e o link deve estar correto para o seu sistema.
- Clique no botão "Instalar" na página de extensão C# DevKit. Isso abre o código do Visual Studio e pergunta se queres instalar ou ativar a extensão. Selecione "instalar".
Cenários para correspondência de padrões
O desenvolvimento moderno geralmente inclui a integração de dados de várias fontes e a apresentação de informações e insights desses dados em um único aplicativo coeso. Você e a sua equipa talvez não controlem os tipos que representam dados recebidos.
A abordagem clássica de design orientado a objetos exige a criação de tipos na sua aplicação que representem cada tipo de dados a partir dessas múltiplas fontes de dados. Depois, a tua aplicação trabalha com esses novos tipos, constrói hierarquias de herança, cria métodos virtuais e implementa abstrações. Essas técnicas funcionam e, às vezes, são as melhores ferramentas. Outras vezes você pode escrever menos código. Em alguns casos, separar os dados das operações que os utilizam pode tornar o código mais fácil de ler.
Neste tutorial, crias e exploras uma aplicação que recebe dados recebidos de várias fontes externas para um único cenário. Vê-se como a correspondência de padrões oferece uma forma eficiente de consumir e processar esses dados de formas que não faziam parte do sistema original.
Considere uma grande área metropolitana que utiliza portagens e preços em horas de ponta para gerir o tráfego. Você escreve um aplicativo que calcula pedágios para um veículo com base em seu tipo. Melhorias posteriores incorporam preços baseados no número de ocupantes do veículo. Outras melhorias adicionam preços com base na hora e no dia da semana.
A partir dessa breve descrição, pode rapidamente esboçar uma hierarquia de objetos para modelar este sistema. No entanto, os seus dados provêm de várias fontes, como outros sistemas de gestão de registo de veículos. Estes sistemas fornecem diferentes classes para modelar esses dados, e esses sistemas não partilham um único modelo de objeto. Neste tutorial, utilizas estas classes simplificadas para modelar os dados do veículo a partir destes sistemas externos, como mostrado no seguinte código:
namespace ConsumerVehicleRegistration
{
public class Car
{
public int Passengers { get; set; }
}
}
namespace CommercialRegistration
{
public class DeliveryTruck
{
public int GrossWeightClass { get; set; }
}
}
namespace LiveryRegistration
{
public class Taxi
{
public int Fares { get; set; }
}
public class Bus
{
public int Capacity { get; set; }
public int Riders { get; set; }
}
}
Você pode baixar o código inicial do repositório dotnet/samples do GitHub. Podes ver que as classes de veículos são de sistemas diferentes e estão em espaços de nomes distintos. Não podes usar uma classe base comum que não seja System.Object.
Designs de correspondência de padrões
O cenário utilizado neste tutorial destaca os tipos de problemas que a correspondência de padrões resolve bem:
- Os objetos com os quais você precisa trabalhar não estão em uma hierarquia de objetos que corresponda às suas metas. Podes estar a trabalhar com classes que fazem parte de sistemas não relacionados.
- A funcionalidade que você está adicionando não faz parte da abstração principal dessas classes. O pedágio pago por um veículo muda para diferentes tipos de veículos, mas o pedágio não é uma função essencial do veículo.
Quando a forma dos dados e as operações nesses dados não são descritas juntas, os recursos de correspondência de padrões em C# facilitam o trabalho.
Implementar os cálculos básicos das portagens
O cálculo mais básico das portagens baseia-se apenas no tipo de veículo:
- A
Carcusta 2,00 €. - A
Taxié 3,50 $. - A
Buscusta 5,00 €. - Um
DeliveryTrucké $10.00.
Crie uma nova TollCalculator classe e implemente a correspondência de padrões no tipo de veículo para obter o valor do pedágio. O código a seguir mostra a implementação inicial do TollCalculator.
using System;
using CommercialRegistration;
using ConsumerVehicleRegistration;
using LiveryRegistration;
namespace Calculators;
public class TollCalculator
{
public decimal CalculateToll(object vehicle) =>
vehicle switch
{
Car c => 2.00m,
Taxi t => 3.50m,
Bus b => 5.00m,
DeliveryTruck t => 10.00m,
{ } => throw new ArgumentException(message: "Not a known vehicle type", paramName: nameof(vehicle)),
null => throw new ArgumentNullException(nameof(vehicle))
};
}
O código anterior usa uma switch expressão (que não é o mesmo que uma switch instrução) que testa o padrão de declaração. Uma expressão switch começa com a variável, vehicle no código anterior, seguida pela switch palavra-chave. Em seguida, vem todos os braços de interruptor dentro de chaves encaracoladas. A switch expressão faz outros refinamentos na sintaxe que envolve a switch declaração. A case palavra-chave é omitida e o resultado de cada braço é uma expressão. Os dois últimos segmentos mostram uma nova característica da linguagem. O { } caso corresponde a qualquer objeto não nulo que não correspondeu a um braço anterior. Este braço captura quaisquer tipos incorretos passados para este método. O caso { } deve alinhar-se com os casos para cada tipo de veículo. Se a ordem fosse invertida, o { } caso teria precedência. Finalmente, o padrão constante deteta quando null é passado para este método. O null padrão pode ser último porque os outros padrões correspondem apenas a um objeto não nulo do tipo correto.
Pode testar este código usando o seguinte código em Program.cs:
using System;
using CommercialRegistration;
using ConsumerVehicleRegistration;
using LiveryRegistration;
using toll_calculator;
var tollCalc = new TollCalculator();
var car = new Car();
var taxi = new Taxi();
var bus = new Bus();
var truck = new DeliveryTruck();
Console.WriteLine($"The toll for a car is {tollCalc.CalculateToll(car)}");
Console.WriteLine($"The toll for a taxi is {tollCalc.CalculateToll(taxi)}");
Console.WriteLine($"The toll for a bus is {tollCalc.CalculateToll(bus)}");
Console.WriteLine($"The toll for a truck is {tollCalc.CalculateToll(truck)}");
try
{
tollCalc.CalculateToll("this will fail");
}
catch (ArgumentException e)
{
Console.WriteLine("Caught an argument exception when using the wrong type");
}
try
{
tollCalc.CalculateToll(null!);
}
catch (ArgumentNullException e)
{
Console.WriteLine("Caught an argument exception when using null");
}
Esse código está incluído no projeto inicial, mas está comentado. Remove os comentários e podes testar o que escreveste.
Você está começando a ver como os padrões podem ajudá-lo a criar algoritmos onde o código e os dados estão separados. A switch expressão testa o tipo e produz valores diferentes com base nos resultados. Isso é apenas o começo.
Adicionar preços de ocupação
A autoridade responsável pelas portagens quer incentivar os veículos a circular na capacidade máxima. Decidem cobrar mais quando os veículos têm menos passageiros e incentivam a utilização de veículos completos oferecendo preços mais baixos:
- Carros e táxis sem passageiros pagam um extra de US $ 0,50.
- Carros e táxis com dois passageiros recebem um desconto de US $ 0,50.
- Carros e táxis com três ou mais passageiros recebem um desconto de US $ 1,00.
- Ônibus que estão menos de 50% cheios pagam um extra de US $ 2,00.
- Ônibus que estão mais de 90% cheios recebem um desconto de US $ 1,00.
Pode implementar estas regras usando um padrão de propriedades na mesma expressão switch. Um padrão de propriedade compara um valor de propriedade a um valor constante. O padrão de propriedades examina as propriedades do objeto uma vez determinado o tipo.
Car {Passengers: 0} é ele próprio um padrão recursivo: o padrão de propriedade exterior em Car contém um padrão constante interno que testa o valor de Passengers. O caso único para um Car expande-se para quatro casos diferentes:
vehicle switch
{
Car {Passengers: 0} => 2.00m + 0.50m,
Car {Passengers: 1} => 2.0m,
Car {Passengers: 2} => 2.0m - 0.50m,
Car => 2.00m - 1.0m,
// ...
};
Os três primeiros casos testam o tipo como um Car, e, em seguida, verificam o valor da propriedade Passengers. Se ambas coincidirem, a expressão é avaliada e devolve.
Também pode expandir os casos para táxis de forma semelhante:
vehicle switch
{
// ...
Taxi {Fares: 0} => 3.50m + 1.00m,
Taxi {Fares: 1} => 3.50m,
Taxi {Fares: 2} => 3.50m - 0.50m,
Taxi => 3.50m - 1.00m,
// ...
};
Em seguida, implemente as regras de ocupação expandindo os casos para ônibus, como mostrado no exemplo a seguir:
vehicle switch
{
// ...
Bus b when ((double)b.Riders / (double)b.Capacity) < 0.50 => 5.00m + 2.00m,
Bus b when ((double)b.Riders / (double)b.Capacity) > 0.90 => 5.00m - 1.00m,
Bus => 5.00m,
// ...
};
A autoridade de pedágio não está preocupada com o número de passageiros nos caminhões de entrega. Em vez disso, ajustam o valor da portagem com base na classe de peso dos camiões da seguinte forma:
- Caminhões com mais de 5000 libras são cobrados um extra de US $ 5,00.
- Caminhões leves com menos de 3000 libras recebem um desconto de US $ 2,00.
Pode implementar essa regra com o seguinte código:
vehicle switch
{
// ...
DeliveryTruck t when (t.GrossWeightClass > 5000) => 10.00m + 5.00m,
DeliveryTruck t when (t.GrossWeightClass < 3000) => 10.00m - 2.00m,
DeliveryTruck => 10.00m,
};
O código anterior mostra a when cláusula de um braço de interruptor. Utilize a cláusula when para testar condições diferentes da igualdade numa propriedade. Quando termina, tem um método que se assemelha muito ao seguinte código:
vehicle switch
{
Car {Passengers: 0} => 2.00m + 0.50m,
Car {Passengers: 1} => 2.0m,
Car {Passengers: 2} => 2.0m - 0.50m,
Car => 2.00m - 1.0m,
Taxi {Fares: 0} => 3.50m + 1.00m,
Taxi {Fares: 1} => 3.50m,
Taxi {Fares: 2} => 3.50m - 0.50m,
Taxi => 3.50m - 1.00m,
Bus b when ((double)b.Riders / (double)b.Capacity) < 0.50 => 5.00m + 2.00m,
Bus b when ((double)b.Riders / (double)b.Capacity) > 0.90 => 5.00m - 1.00m,
Bus => 5.00m,
DeliveryTruck t when (t.GrossWeightClass > 5000) => 10.00m + 5.00m,
DeliveryTruck t when (t.GrossWeightClass < 3000) => 10.00m - 2.00m,
DeliveryTruck => 10.00m,
{ } => throw new ArgumentException(message: "Not a known vehicle type", paramName: nameof(vehicle)),
null => throw new ArgumentNullException(nameof(vehicle))
};
Como referido anteriormente, estes ramos de switch são padrões recursivos, que aninham um padrão constante dentro de um padrão de propriedades.
Você pode tornar esse código menos repetitivo usando switches aninhados. O Car e Taxi ambos têm quatro braços diferentes nos exemplos anteriores. Em ambos os casos, pode-se criar um padrão de declaração que contribui para um padrão constante. Esta técnica é mostrada no código a seguir:
public decimal CalculateToll(object vehicle) =>
vehicle switch
{
Car c => c.Passengers switch
{
0 => 2.00m + 0.5m,
1 => 2.0m,
2 => 2.0m - 0.5m,
_ => 2.00m - 1.0m
},
Taxi t => t.Fares switch
{
0 => 3.50m + 1.00m,
1 => 3.50m,
2 => 3.50m - 0.50m,
_ => 3.50m - 1.00m
},
Bus b when ((double)b.Riders / (double)b.Capacity) < 0.50 => 5.00m + 2.00m,
Bus b when ((double)b.Riders / (double)b.Capacity) > 0.90 => 5.00m - 1.00m,
Bus b => 5.00m,
DeliveryTruck t when (t.GrossWeightClass > 5000) => 10.00m + 5.00m,
DeliveryTruck t when (t.GrossWeightClass < 3000) => 10.00m - 2.00m,
DeliveryTruck t => 10.00m,
{ } => throw new ArgumentException(message: "Not a known vehicle type", paramName: nameof(vehicle)),
null => throw new ArgumentNullException(nameof(vehicle))
};
Na amostra anterior, o aninhamento de uma expressão switch dentro de outro braço de switch evita repetir os braços Car e Taxi que contêm braços filhos que testam o valor da propriedade. Esta técnica não é usada para os Bus braços e DeliveryTruck porque esses braços testam intervalos para a propriedade, não valores discretos.
Adicionar preços de pico
Para a última funcionalidade, a autoridade de portagens quer adicionar tarifação de ponta variável consoante a hora. Durante as horas de ponta da manhã e da noite, as portagens são duplicadas. Essa regra afeta apenas o tráfego em um sentido: entrada para a cidade pela manhã e saída na hora do rush da noite. Nos demais horários da jornada de trabalho, os pedágios aumentam 50%. No final da noite e madrugada, as portagens são reduzidas em 25%. Durante o fim de semana, é a taxa normal, independentemente do horário. Pode usar uma série de instruções if e else para expressar esta regra usando o seguinte código:
public decimal PeakTimePremiumIfElse(DateTime timeOfToll, bool inbound)
{
if ((timeOfToll.DayOfWeek == DayOfWeek.Saturday) ||
(timeOfToll.DayOfWeek == DayOfWeek.Sunday))
{
return 1.0m;
}
else
{
int hour = timeOfToll.Hour;
if (hour < 6)
{
return 0.75m;
}
else if (hour < 10)
{
if (inbound)
{
return 2.0m;
}
else
{
return 1.0m;
}
}
else if (hour < 16)
{
return 1.5m;
}
else if (hour < 20)
{
if (inbound)
{
return 1.0m;
}
else
{
return 2.0m;
}
}
else // Overnight
{
return 0.75m;
}
}
}
O código anterior funciona corretamente, mas não é legível. Você tem que percorrer todos os casos de entrada e as declarações aninhadas if para raciocinar sobre o código. Em vez disso, você usará a correspondência de padrões para esse recurso, mas o integrará com outras técnicas. Poderias construir uma única expressão de correspondência de padrão que tenha em conta todas as combinações de direção, dia da semana e hora. O resultado seria uma expressão complicada. Seria difícil de ler e difícil de entender. Isso torna difícil garantir a correção. Em vez disso, combine esses métodos para construir uma tupla de valores que descreva de forma concisa todos esses estados. Em seguida, use a correspondência de padrões para calcular um multiplicador para o pedágio. A tupla contém três condições discretas:
- O dia é um dia de semana ou um fim de semana.
- A faixa de tempo em que o pedágio é cobrado.
- A direção é para dentro ou para fora da cidade.
A tabela a seguir mostra as combinações de valores de entrada e o multiplicador de preços de pico:
| Dia | Hora | Direction | Premium |
|---|---|---|---|
| Dia da Semana | Corrida matinal | inbound | x 2,00 |
| Dia da Semana | Corrida matinal | outbound | x 1,00 |
| Dia da Semana | durante o dia | inbound | x 1,50 |
| Dia da Semana | durante o dia | outbound | x 1,50 |
| Dia da Semana | Corrida noturna | inbound | x 1,00 |
| Dia da Semana | Corrida noturna | outbound | x 2,00 |
| Dia da Semana | durante a noite | inbound | x 0,75 |
| Dia da Semana | durante a noite | outbound | x 0,75 |
| Fim de semana | Corrida matinal | inbound | x 1,00 |
| Fim de semana | Corrida matinal | outbound | x 1,00 |
| Fim de semana | durante o dia | inbound | x 1,00 |
| Fim de semana | durante o dia | outbound | x 1,00 |
| Fim de semana | Corrida noturna | inbound | x 1,00 |
| Fim de semana | Corrida noturna | outbound | x 1,00 |
| Fim de semana | durante a noite | inbound | x 1,00 |
| Fim de semana | durante a noite | outbound | x 1,00 |
Existem 16 combinações diferentes das três variáveis. Ao combinar algumas das condições, simplifica a expressão final do switch.
O sistema que cobra as portagens utiliza uma DateTime estrutura para o momento em que a portagem foi cobrada. Desenvolva métodos de membro que criem as variáveis da tabela anterior. A função a seguir usa uma expressão de correspondência de padrão para determinar se um DateTime representa um fim de semana ou um dia da semana.
private static bool IsWeekDay(DateTime timeOfToll) =>
timeOfToll.DayOfWeek switch
{
DayOfWeek.Monday => true,
DayOfWeek.Tuesday => true,
DayOfWeek.Wednesday => true,
DayOfWeek.Thursday => true,
DayOfWeek.Friday => true,
DayOfWeek.Saturday => false,
DayOfWeek.Sunday => false
};
Esse método é correto, mas é repetitivo. Você pode simplificá-lo, conforme mostrado no código a seguir:
private static bool IsWeekDay(DateTime timeOfToll) =>
timeOfToll.DayOfWeek switch
{
DayOfWeek.Saturday => false,
DayOfWeek.Sunday => false,
_ => true
};
Em seguida, adicione uma função semelhante para categorizar o tempo nos blocos:
private enum TimeBand
{
MorningRush,
Daytime,
EveningRush,
Overnight
}
private static TimeBand GetTimeBand(DateTime timeOfToll) =>
timeOfToll.Hour switch
{
< 6 or > 19 => TimeBand.Overnight,
< 10 => TimeBand.MorningRush,
< 16 => TimeBand.Daytime,
_ => TimeBand.EveningRush,
};
Você adiciona um privado enum para converter cada intervalo de tempo em um valor discreto. Depois, o GetTimeBand método utiliza padrões relacionais e padrões lógicos. Um padrão relacional permite testar um valor numérico usando <, >, <=, ou >=. Padrões lógicos combinam outros padrões: um or padrão testa se uma expressão corresponde a um ou mais padrões, um and padrão testa se uma expressão corresponde a dois padrões distintos, e um not padrão testa se uma expressão não corresponde a um padrão.
Depois de criar esses métodos, pode usar outra switch expressão com um padrão de tuplas — um padrão posicional que corresponde a cada elemento de uma tupla — para calcular o prémio de preços. Você pode construir uma switch expressão contendo todos os 16 braços.
public decimal PeakTimePremiumFull(DateTime timeOfToll, bool inbound) =>
(IsWeekDay(timeOfToll), GetTimeBand(timeOfToll), inbound) switch
{
(true, TimeBand.MorningRush, true) => 2.00m,
(true, TimeBand.MorningRush, false) => 1.00m,
(true, TimeBand.Daytime, true) => 1.50m,
(true, TimeBand.Daytime, false) => 1.50m,
(true, TimeBand.EveningRush, true) => 1.00m,
(true, TimeBand.EveningRush, false) => 2.00m,
(true, TimeBand.Overnight, true) => 0.75m,
(true, TimeBand.Overnight, false) => 0.75m,
(false, TimeBand.MorningRush, true) => 1.00m,
(false, TimeBand.MorningRush, false) => 1.00m,
(false, TimeBand.Daytime, true) => 1.00m,
(false, TimeBand.Daytime, false) => 1.00m,
(false, TimeBand.EveningRush, true) => 1.00m,
(false, TimeBand.EveningRush, false) => 1.00m,
(false, TimeBand.Overnight, true) => 1.00m,
(false, TimeBand.Overnight, false) => 1.00m,
};
O código anterior funciona, mas pode ser simplificado. Todas as oito combinações para o fim de semana têm o mesmo pedágio. Você pode substituir todos os oito pela seguinte linha:
(false, _, _) => 1.0m,
Tanto o tráfego de entrada como o de saída têm o mesmo multiplicador durante o dia da semana e durante a noite. Pode substituir esses quatro braços do switch pelas seguintes duas linhas:
(true, TimeBand.Overnight, _) => 0.75m,
(true, TimeBand.Daytime, _) => 1.5m,
O código deve se parecer com o código a seguir após essas duas alterações:
public decimal PeakTimePremium(DateTime timeOfToll, bool inbound) =>
(IsWeekDay(timeOfToll), GetTimeBand(timeOfToll), inbound) switch
{
(true, TimeBand.MorningRush, true) => 2.00m,
(true, TimeBand.MorningRush, false) => 1.00m,
(true, TimeBand.Daytime, _) => 1.50m,
(true, TimeBand.EveningRush, true) => 1.00m,
(true, TimeBand.EveningRush, false) => 2.00m,
(true, TimeBand.Overnight, _) => 0.75m,
(false, _, _) => 1.00m,
};
Por fim, elimine os dois horários de hora de ponta que pagam o preço normal. Depois de remover esses braços, substitua false por um ramal de descarga (_) no braço final do interruptor. Obtém o seguinte método concluído:
public decimal PeakTimePremium(DateTime timeOfToll, bool inbound) =>
(IsWeekDay(timeOfToll), GetTimeBand(timeOfToll), inbound) switch
{
(true, TimeBand.Overnight, _) => 0.75m,
(true, TimeBand.Daytime, _) => 1.5m,
(true, TimeBand.MorningRush, true) => 2.0m,
(true, TimeBand.EveningRush, false) => 2.0m,
_ => 1.0m,
};
Este exemplo destaca uma das vantagens da correspondência de padrões: as ramificações de padrão são avaliadas em ordem. Se reorganizares as instruções para que um ramo anterior trate um dos teus casos posteriores, o compilador avisa-te sobre o código inacessível. Essas regras de linguagem tornaram mais fácil fazer as simplificações anteriores com a confiança de que o código não foi alterado.
A correspondência de padrões torna alguns tipos de código mais legíveis e oferece uma alternativa às técnicas orientadas a objetos quando você não pode adicionar código às suas classes. A nuvem está a fazer com que os dados e a funcionalidade vivam separados. A forma dos dados e as operações neles não são necessariamente descritas em conjunto. Neste tutorial, você consumiu dados existentes de maneiras totalmente diferentes de sua função original. A correspondência de padrões lhe deu a capacidade de escrever funcionalidades que substituíam esses tipos, mesmo que você não pudesse estendê-los.
Passos seguintes
Você pode baixar o código concluído do repositório dotnet/samples do GitHub. Explore padrões por conta própria e adicione essa técnica às suas atividades regulares de codificação. Aprender estas técnicas dá-lhe outra forma de abordar problemas e criar novas funcionalidades.