A partir do C# 7.0 a linguagem começou a receber funcionalidades que fazem uso de um recurso de programação funcional chamado pattern matching. A maioria desses recursos é derivada do F# e a introdução deles no C# visa facilitar a vida do desenvolvedor e solução de certos tipos de problemas.
São recursos muito poderosos e que simplificam em muito alguns tipos de construções.
Como os recursos de pattern matching do C# 8.0 são extensões daqueles instroduzidos no C# 7.0, vou apresentá-los na mesma ordem para simplificar o entendimento.
Patterns introduzidos no C# 7.0
No C# 7.0 o operador is e a declaração switch foram melhoradas com a adição de alguns novos padrões: const pattern, type pattern e var pattern.
Const pattern
Vejamos a seguinte função, que recebe um objeto o
como argumento:
public void Is(object o)
{
if (o is null) Console.WriteLine("o é null!");
// ...
}
Antes do C# 7.0, teríamos que escrever essa verificação usando o operador ==
, mas agora podemos fazer a mesma validação com o operador is.
Este padrão também vale para inteiros ou outros valores constantes:
if (o is 42) Console.WriteLine("A resposta é 42!");
Type pattern
O type pattern pode ser um pouco mais útil. Com ele podemos extrair uma variável ao verificar o tipo, tanto com o operador is quanto com o switch.
Este padrão sempre tem sucesso quando o tipo aferido é o mesmo da variável.
public void Is(object o)
{
if (o is Pessoa pessoa)
Console.WriteLine($"O nome é {pessoa.Nome}");
// ...
}
Com o tipo identificado, também é possível usá-lo na mesma expressão para filtrar ainda mais o objeto:
if (o is Pessoa p && p.Idade < 18)
Console.WriteLine($"{p.Nome} é menor de idade.");
Var pattern
O var pattern sempre é avaliado com sucesso, pois toda variável tem um tipo.
Ele é bastante útil para obter uma variável já convertida para o seu tipo verdadeiro.
public void Is(object o)
{
if (o is var x) {
var nomeTipo = x?.GetType()?.Name;
Console.WriteLine($"O tipo de 'x' é {nomeTipo}");
}
// ...
}
Então, se passarmos uma variável do tipo Pessoa
para a variável x
no exemplo acima, a saída será O tipo de 'x' é Pessoa
.
Usando a declaração switch com Pattern Matching
Todos os três padrões acima são utilizáveis numa declaração do tipo switch:
switch(o)
{
case Humano humano:
Console.WriteLine($"Nome do humano: {humano.Nome}");
break;
case AnimalSelvagem animal:
Console.WriteLine($"Nome do animal: {animal.Nome}");
break;
case int resposta:
Console.WriteLine($"A resposta é {resposta}");
break;
case var x:
Console.WriteLine($"x não é incógnita! É do tipo {x.GetType().Name}!");
break;
}
Como o var pattern sempre dá match, pode até ser usado como default
.
Recursos introduzidos no C# 8.0
Já no C# 8.0, foi introduzido um pattern matching bem mais poderoso, mais parecido com o encontrado em linguagens como F# e Scala.
Antes de ver um exemplo completo, é interessante fazermos uma rápida revisão de tuplas tal como são usadas a partir do C# 7.0.
Uma tupla pode ser declarada assim:
var tupla = (1, true); // tupla do tipo (int, bool)
E uma tupla pode ser facilmente decomposta desta forma:
(var entrada, var saida = (1, true);
// ou
(int entrada, bool saida = (1, true);
O primeiro valor da tupla é atribuído à primeira variável, e o segundo valor é atribuído à segunda variável.
Outro aspecto importante, é a possibilidade de utilizar o operador de descarte _
.
(var a, _) = (x, y);
Neste caso, comunicamos ao compilador que não estamos interessados no segundo valor da tupla, e que ele pode ser descartado.
Full pattern matching!
No C# 8.0, é possível escrever uma declaração switch em que cada caso é uma expressão que será avaliada em tempo de execução.
Vejamos um exemplo para selecionar uma classe CSS com base no valor de uma variável do tipo bool?
:
bool? visivel = false;
var classeCss = visivel switch
{
true => "Visible",
false => "Hidden",
null => "Blink"
};
Neste exemplo, ao rodar este código, cada uma das expressões será avaliada, na ordem em que foram declaradas. A primeira que for verdadeira (der match) será executada.
Há dois pontos interessantes para notar: o primeiro é que neste caso a expressão switch
possui um retorno que pode ser atribuído a uma variável ou retornado. O segundo ponto é que ela está usando o const pattern que vimos mais acima.
A entrada de uma expressão swtich com pattern matching pode ser uma tupla, e cada caso pode ser decomposto. Este padrão é chamado de tuple pattern. Vamos a um exemplo mais complexo:
int a = A(); // valor entre 1 e 3
int b = B(); // valor entre 1 e 3
bool c = HabilitarAvaliacao();
bool resultado = (a, b, c) switch
{
(1, 1, false) => Ok(),
(1, 2, false) => Ok(),
(1, 3, false) => Ok(),
(2, 1, false) => Ok(),
(2, 2, false) => Ok(),
(2, 3, false) => Ok(),
(3, 1, false) => Ok(),
(3, 2, false) => Ok(),
(3, 3, false) => Ok(),
(1, 3, true) => Ok(),
(2, 3, true) => Ok(),
(3, 3, true) => Ok(),
(_, _, _) => Nok()
};
Neste exemplo, estamos tratando várias combinações de variáveis que resultam numa avaliação Ok()
, e em outros casos resultam num Nok()
;
Este código pode ser fácil de ler e entender, mas pode ser simplificado com a introdução de outro recurso, as guardas. Vejamos um exemplo:
var resultado = (1, 2) switch
{
(var a, var b) when a > 10 => a * b,
(var a, var b) when b < 1 => a + b,
(_, _) => return a - b
};
Neste caso, apesar de termos dois casos aparentemente idênticos (var a, var b)
, o primeiro só será executado quando a
for maior que 10
. Caso não for, o segundo poderá ser executado quando b
for menor que 1
.
Assim, o exemplo anterior poderia ser reescrito, tomando vantagem das guardas:
int a = A(); // valor entre 1 e 3
int b = B(); // valor entre 1 e 3
bool c = HabilitarAvaliacao();
bool resultado = (a, b, c) switch
{
(_, _, false) => Ok(),
(_, 3, true) => Ok(),
(_, _, _) => Nok()
};
Vamos a um exemplo mais prático, em que fazemos um tratamento para checar a saúde de um serviço, e avaliação toma dois argumentos: 1) o sucesso na checagem e 2) o tempo de resposta do serviço:
var healthCheckResult = (sucesso, tempoRespostaMilissegundos) switch
{
(true, var ms) when ms <= 5000 => HealthCheckResult.Healthy(),
(true, _) => HealthCheckResult.Degraded(),
(false, _) => HealthCheckResult.Unhealthy(),
};
No cenário acima, temos um uso interessante da guarda: o serviço só será considerado saudável quando o tempo de resposta for de até 5 segundos.
Esta é uma forma bastante compacta e concisa de representar vários cenários num código expressivo e fácil de ler, e é particularmente útil para reescrever cadeias complexas ou confusas de if
s.
Implementando máquinas de estado
Esta abordagem também é bastante útil para descrever máquinas de estado.
Por exemplo, uma máquina de estado para um portão eletrônico poderia ser algo mais ou menos assim:
var newState = (currentState, action, key.IsValid) switch {
(GateState.Locked, GateAction.Open, true) => GateState.Opened,
(GateState.Opened, GateAction.Open, _) => throw new InvalidOperationException("Não se pode abrir um portão aberto"),
(GateState.Opened, GateAction.Lock, true) => GateState.Locked,
(GateState.Locked, GateAction.Open, false) => GateState.Locked,
(GateState.Closed, GateAction.Lock, true) => GateState.Locked,
(GateState.Closed, GateAction.Close, _) => throw new InvalidOperationException("Não se pode fechar um portão fechado"),
_ => currentState
};
Desconstrução de tipos
Um último recurso que eu gostaria de mostrar aqui é a desconstrução de tipos, ou seja, é um structure pattern.
Para exemplificar, veja esses dois structs que representam respectivamente pontos em sistemas de coordenadas 2D e 3D:
public struct Point2D
{
public float X { get; }
public float Y { get; }
public Point2D(float x, float y)
{
X = x;
Y = y;
}
}
public struct Point3D
{
public float X { get; }
public float Y { get; }
public float Z { get; }
public Point3D(float x, float y, float z)
{
X = x;
Y = y;
Z = z;
}
}
Ambos possuem as coordenadas X
e Y
, mas somente o Point3D
possui a coordenada Z
. Assim, podemos usar pattern matching para avaliar a estrutura dos objetos e reagir de acordo:
object ponto = ObterPonto();
var estaNaOrigem = ponto switch
{
Point3D {X: 0, Y: 0, Z: 0} => true,
Point2D {X: 0, Y: 0} => true,
null => false
};
E se o objeto for de um tipo conhecido, o tipo não precisa nem ser citado:
var ponto = new Point3D(x: 0, y: 2, z: 3);
var planoAlinhado = ponto switch
{
{X: 0, Z: 0} => Plano.XZ,
{X: 0, Y: 0} => Plano.XY,
{Y: 0, Z: 0} => Plano.YZ,
_ => Plano.Nenhum
};
Por hoje é isso. Se você conhece outras formas interessantes de aplicar pattern matching em C# ou outra linguagem de programação que você goste, deixe um comentário.