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.
Sugestão
Novo no desenvolvimento de software? Começa primeiro pelos tutoriais para começar . Introduzem classes, métodos e fluxo de controlo.
Experiente noutra língua? Este tutorial mostra como os padrões C# podem expressar claramente o comportamento dos objetos quando as regras dependem do estado atual de um objeto.
Neste tutorial, constróis uma aplicação de consola que modela as regras de uma eclusa de canal.
Uma eclusa de canal eleva ou desce barcos entre dois trechos de água a diferentes alturas. Tem duas portas e uma câmara cujo nível de água varia entre uma configuração baixa e uma alta. A eclusa só pode operar em segurança quando o nível da água e as posições da comporta permanecem em combinações válidas.
Neste tutorial, aprenderás como:
- Expresse o comportamento do objeto fazendo correspondência com base no estado.
- Implementa essas regras com correspondência de padrões em C#.
- Use os diagnósticos do compilador para validar a sua implementação.
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".
Construir uma simulação de uma eclusa de canal
Uma eclusa de canal eleva e baixa os barcos entre cursos de água em diferentes níveis. Neste tutorial, a eclusa simulada tem um portão inferior, um portão superior e um nível de água que pode estar baixo ou alto.
Em funcionamento normal, um barco entra quando o nível da água dentro da eclusa coincide com o nível do lado de entrada. Assim que o barco está dentro, ambos os portões fecham-se. O nível da água muda para corresponder ao lado de jusante, e depois a porta de saída abre-se. Para manter o modelo seguro, o nível da água só pode mudar quando ambas as comportas estão fechadas, e uma comporta só pode abrir quando o nível da água corresponder a esse lado.
Podes modelar essas regras com uma CanalLock classe. Expõe comandos para abrir ou fechar qualquer um dos portões e para elevar ou baixar o nível da água. Também expõe propriedades que indicam o estado atual do bloqueio.
Defina a classe
Cria um projeto de consola e depois adiciona uma classe chamada CanalLock. Comece por desenhar a API pública e deixar os métodos por implementar:
public enum WaterLevel
{
Low,
High
}
public class CanalLock
{
// Query canal lock state:
public WaterLevel CanalLockWaterLevel { get; private set; } = WaterLevel.Low;
public bool HighWaterGateOpen { get; private set; } = false;
public bool LowWaterGateOpen { get; private set; } = false;
// Change the upper gate.
public void SetHighGate(bool open)
{
throw new NotImplementedException();
}
// Change the lower gate.
public void SetLowGate(bool open)
{
throw new NotImplementedException();
}
// Change water level.
public void SetWaterLevel(WaterLevel newLevel)
{
throw new NotImplementedException();
}
public override string ToString() =>
$"The lower gate is {(LowWaterGateOpen ? "Open" : "Closed")}. " +
$"The upper gate is {(HighWaterGateOpen ? "Open" : "Closed")}. " +
$"The water level is {CanalLockWaterLevel}.";
}
O código anterior inicializa a eclusa com ambas as comportas fechadas e o nível da água baixo. De seguida, adicione o seguinte código a Main para orientar a sua primeira implementação:
// Create a new canal lock:
var canalGate = new CanalLock();
// State should be doors closed, water level low:
Console.WriteLine(canalGate);
canalGate.SetLowGate(open: true);
Console.WriteLine($"Open the lower gate: {canalGate}");
Console.WriteLine("Boat enters lock from lower gate");
canalGate.SetLowGate(open: false);
Console.WriteLine($"Close the lower gate: {canalGate}");
canalGate.SetWaterLevel(WaterLevel.High);
Console.WriteLine($"Raise the water level: {canalGate}");
canalGate.SetHighGate(open: true);
Console.WriteLine($"Open the higher gate: {canalGate}");
Console.WriteLine("Boat exits lock at upper gate");
Console.WriteLine("Boat enters lock from upper gate");
canalGate.SetHighGate(open: false);
Console.WriteLine($"Close the higher gate: {canalGate}");
canalGate.SetWaterLevel(WaterLevel.Low);
Console.WriteLine($"Lower the water level: {canalGate}");
canalGate.SetLowGate(open: true);
Console.WriteLine($"Open the lower gate: {canalGate}");
Console.WriteLine("Boat exits lock at upper gate");
canalGate.SetLowGate(open: false);
Console.WriteLine($"Close the lower gate: {canalGate}");
Agora adicione uma primeira implementação que altera cada valor de estado sem aplicar as regras de segurança:
// Change the upper gate.
public void SetHighGate(bool open)
{
HighWaterGateOpen = open;
}
// Change the lower gate.
public void SetLowGate(bool open)
{
LowWaterGateOpen = open;
}
// Change water level.
public void SetWaterLevel(WaterLevel newLevel)
{
CanalLockWaterLevel = newLevel;
}
Estas primeiras verificações passam. Os mecanismos estão a funcionar. De seguida, adicione um teste para a primeira condição de falha. No final da sequência anterior, ambas as comportas estão fechadas e o nível da água está baixo. Tente abrir o portão superior:
Console.WriteLine("=============================================");
Console.WriteLine(" Test invalid commands");
// Open "wrong" gate (2 tests)
try
{
canalGate = new CanalLock();
canalGate.SetHighGate(open: true);
}
catch (InvalidOperationException)
{
Console.WriteLine("Invalid operation: Can't open the high gate. Water is low.");
}
Console.WriteLine($"Try to open upper gate: {canalGate}");
Esse teste falha porque o portão superior abre quando não devia. Uma primeira solução poderia ser algo assim:
// Change the upper gate.
public void SetHighGate(bool open)
{
if (open && (CanalLockWaterLevel == WaterLevel.High))
HighWaterGateOpen = true;
else if (open && (CanalLockWaterLevel == WaterLevel.Low))
throw new InvalidOperationException("Cannot open high gate when the water is low");
}
Os teus testes são aprovados outra vez. Mas à medida que adicionas mais condições, acumulas mais if instruções. O código torna-se mais difícil de analisar porque cada regra está separada das outras.
Implemente os comandos com padrões
Uma opção mais clara é usar padrões para descrever diretamente as combinações válidas. No passo seguinte, cada expressão de switch usa uma tupla como entrada do padrão. C# avalia essa tupla uma vez, e cada ramo da instrução switch testa o estado atual da comporta, o nível da água e a nova configuração solicitada.
Para o portão superior, podes resumir essas combinações assim:
| Nova configuração | Estado do portão | Nível da água | Result |
|---|---|---|---|
| Fechado | Fechado | Alto | Fechado |
| Fechado | Fechado | Baixo | Fechado |
| Fechado | Abrir | Alto | Fechado |
|
|
|
|
|
| Abrir | Fechado | Alto | Abrir |
| Abrir | Fechado | Baixo | Fechado (erro) |
| Abrir | Abrir | Alto | Abrir |
|
|
|
|
|
As linhas riscadas representam estados internos inválidos. A expressão switch pode codificar diretamente as transições válidas.
false ainda significa que o portão está fechado:
HighWaterGateOpen = (open, HighWaterGateOpen, CanalLockWaterLevel) switch
{
(false, false, WaterLevel.High) => false,
(false, false, WaterLevel.Low) => false,
(false, true, WaterLevel.High) => false,
(false, true, WaterLevel.Low) => false, // should never happen
(true, false, WaterLevel.High) => true,
(true, false, WaterLevel.Low) => throw new InvalidOperationException("Cannot open high gate when the water is low"),
(true, true, WaterLevel.High) => true,
(true, true, WaterLevel.Low) => false, // should never happen
};
Experimente esta versão. Os seus testes foram aprovados. O compilador também alerta que a expressão switch não é exaustiva — não cobre todos os valores possíveis — porque WaterLevel é um enum, e C# permite que qualquer valor do tipo numérico subjacente do enum seja convertido nesse enum, mesmo um sem um membro nomeado. Adicione um braço final com o padrão de descarte (_) para lidar com esses estados internos impossíveis:
_ => throw new InvalidOperationException("Invalid internal state"),
Esse braço tem de ser o último porque o padrão de descarte corresponde a todas as entradas restantes.
Depois podes simplificar as ramificações anteriores. Fechar o portão é sempre permitido, para que um braço possa substituir os quatro casos de fecho separados:
(false, _, _) => false,
Também pode combinar os casos válidos em aberto e preservar o único erro de segurança:
(true, _, WaterLevel.High) => true,
(true, false, WaterLevel.Low) => throw new InvalidOperationException("Cannot open high gate when the water is low"),
_ => throw new InvalidOperationException("Invalid internal state"),
Execute o programa novamente. Os testes ainda passam. Aqui está a implementação final SetHighGate :
// Change the upper gate.
public void SetHighGate(bool open)
{
HighWaterGateOpen = (open, HighWaterGateOpen, CanalLockWaterLevel) switch
{
(false, _, _) => false,
(true, _, WaterLevel.High) => true,
(true, false, WaterLevel.Low) => throw new InvalidOperationException("Cannot open high gate when the water is low"),
_ => throw new InvalidOperationException("Invalid internal state"),
};
}
Implementar as regras restantes
Agora aplique a mesma ideia a SetLowGate e SetWaterLevel. Comece por adicionar testes que expõem operações inválidas:
Console.WriteLine();
Console.WriteLine();
try
{
canalGate = new CanalLock();
canalGate.SetWaterLevel(WaterLevel.High);
canalGate.SetLowGate(open: true);
}
catch (InvalidOperationException)
{
Console.WriteLine("invalid operation: Can't open the lower gate. Water is high.");
}
Console.WriteLine($"Try to open lower gate: {canalGate}");
// change water level with gate open (2 tests)
Console.WriteLine();
Console.WriteLine();
try
{
canalGate = new CanalLock();
canalGate.SetLowGate(open: true);
canalGate.SetWaterLevel(WaterLevel.High);
}
catch (InvalidOperationException)
{
Console.WriteLine("invalid operation: Can't raise water when the lower gate is open.");
}
Console.WriteLine($"Try to raise water with lower gate open: {canalGate}");
Console.WriteLine();
Console.WriteLine();
try
{
canalGate = new CanalLock();
canalGate.SetWaterLevel(WaterLevel.High);
canalGate.SetHighGate(open: true);
canalGate.SetWaterLevel(WaterLevel.Low);
}
catch (InvalidOperationException)
{
Console.WriteLine("invalid operation: Can't lower water when the high gate is open.");
}
Console.WriteLine($"Try to lower water with high gate open: {canalGate}");
Execute o aplicativo novamente. Estes testes falham e a eclusa do canal atinge estados inválidos. Implemente os métodos restantes fazendo correspondência com base no estado completo.
SetLowGate é semelhante a SetHighGate.
SetWaterLevel Utiliza o nível de água atual mais ambas as posições das comportas:
CanalLockWaterLevel = (newLevel, CanalLockWaterLevel, LowWaterGateOpen, HighWaterGateOpen) switch
{
// arms go here
};
Tens 16 combinações a considerar. Comece com a tabela completa, escreva os casos da instrução switch, execute os testes e depois simplifique os resultados repetidos.
Acabou por usar métodos semelhantes a estes?
// Change the lower gate.
public void SetLowGate(bool open)
{
LowWaterGateOpen = (open, LowWaterGateOpen, CanalLockWaterLevel) switch
{
(false, _, _) => false,
(true, _, WaterLevel.Low) => true,
(true, false, WaterLevel.High) => throw new InvalidOperationException("Cannot open low gate when the water is high"),
_ => throw new InvalidOperationException("Invalid internal state"),
};
}
// Change water level.
public void SetWaterLevel(WaterLevel newLevel)
{
CanalLockWaterLevel = (newLevel, CanalLockWaterLevel, LowWaterGateOpen, HighWaterGateOpen) switch
{
(WaterLevel.Low, WaterLevel.Low, true, false) => WaterLevel.Low,
(WaterLevel.High, WaterLevel.High, false, true) => WaterLevel.High,
(WaterLevel.Low, _, false, false) => WaterLevel.Low,
(WaterLevel.High, _, false, false) => WaterLevel.High,
(WaterLevel.Low, WaterLevel.High, false, true) => throw new InvalidOperationException("Cannot lower water when the high gate is open"),
(WaterLevel.High, WaterLevel.Low, true, false) => throw new InvalidOperationException("Cannot raise water when the low gate is open"),
_ => throw new InvalidOperationException("Invalid internal state"),
};
}
Os seus testes deverão agora passar, e a eclusa do canal deve aplicar as suas regras de segurança.
Resumo
Neste tutorial, usaste a correspondência estrutural para expressar como um objeto pode mudar de um estado válido para outro. Os padrões mantinham as transições permitidas juntas, por isso podias compará-las mais facilmente do que com uma longa série de instruções ramificadas. Esta abordagem funciona bem quando o comportamento depende da forma combinada de vários valores de estado.