Dyego Maas - Blog

Generative AI Consultant and Software Architect

Pattern matching in C# 8.0

Pattern matching in C# 8.0

Pattern matching in C#? In this post we explore how this functional programming feature can improve our C# 8.0 programs.

9 min read

Starting with C# 7.0, the language began getting features that rely on a functional programming technique called pattern matching. Most of them come from F#, and bringing them into C# is meant to make developers’ lives easier and help solve certain kinds of problems.

They’re very powerful features that greatly simplify some kinds of constructs.

Since the pattern matching features in C# 8.0 extend the ones introduced in C# 7.0, I’ll present them in that same order to keep things easy to follow.

Patterns introduced in C# 7.0

In C# 7.0 the is operator and the switch statement were improved with a few new patterns: the const pattern, the type pattern and the var pattern.

Const pattern

Take the following function, which receives an object o as an argument:

public void Is(object o)
{
  if (o is null) Console.WriteLine("o é null!");
  // ...
}

Before C# 7.0, we’d have to write this check with the == operator, but now we can do the same check with the is operator.

This pattern also works for integers and other constant values:

if (o is 42) Console.WriteLine("A resposta é 42!");

Type pattern

The type pattern can be a bit more useful. With it we can extract a variable while checking the type, with both the is operator and switch.

This pattern always succeeds when the type being checked is the same as the variable’s.

public void Is(object o)
{
  if (o is Pessoa pessoa)
      Console.WriteLine($"O nome é {pessoa.Nome}");
  // ...
}

Once the type is identified, you can also use it in the same expression to filter the object further:

if (o is Pessoa p && p.Idade < 18)
  Console.WriteLine($"{p.Nome} é menor de idade.");

Var pattern

The var pattern always succeeds, since every variable has a type.

It’s quite handy for getting a variable already cast to its actual type.

public void Is(object o)
{
  if (o is var x) {
      var nomeTipo = x?.GetType()?.Name;
      Console.WriteLine($"O tipo de 'x' é {nomeTipo}");
  }
  // ...
}

So if we pass a variable of type Pessoa (Person) as x in the example above, the output will be O tipo de 'x' é Pessoa (“The type of ‘x’ is Pessoa”).

Using the switch statement with Pattern Matching

All three patterns above can be used in a switch statement:

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;
}

Since the var pattern always matches, it can even be used as the default.

Features introduced in C# 8.0

C# 8.0 introduced much more powerful pattern matching, closer to what you find in languages like F# and Scala.

Before looking at a full example, it’s worth a quick refresher on tuples as they’re used since C# 7.0.

A tuple can be declared like this:

var tupla = (1, true); // tuple of type (int, bool)

And a tuple can easily be deconstructed like this:

(var entrada, var saida) = (1, true);
// or
(int entrada, bool saida) = (1, true);

The tuple’s first value is assigned to the first variable, and the second value to the second variable.

Another important aspect is being able to use the discard operator _.

(var a, _) = (x, y);

Here, we tell the compiler we’re not interested in the tuple’s second value and that it can be discarded.

Full pattern matching!

In C# 8.0, you can write a switch in which each case is an expression evaluated at runtime.

Let’s look at an example that picks a CSS class based on the value of a bool? variable:

bool? visivel = false;
var classeCss = visivel switch
{
true => "Visible",
false => "Hidden",
null => "Blink"
};

In this example, when the code runs, each expression is evaluated in the order it was declared. The first one that’s true (that matches) is executed.

There are two interesting things to notice: first, here the switch expression returns a value that can be assigned to a variable or returned. Second, it’s using the const pattern we saw above.

The input of a switch expression with pattern matching can be a tuple, and each case can be deconstructed. This is called the tuple pattern. Let’s look at a more complex example:

int a = A(); // value between 1 and 3
int b = B(); // value between 1 and 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()
};

In this example, we’re handling several combinations of variables that result in an Ok() evaluation, while other cases result in a Nok().

This code may be easy to read and understand, but it can be simplified with another feature: guards. Let’s look at an example:

var resultado = (1, 2) switch
{
  (var a, var b) when a > 10 => a * b,
  (var a, var b) when b < 1 => a + b,
  (_, _) => a - b 
};

Here, even though we have two seemingly identical cases (var a, var b), the first one only runs when a is greater than 10. If it isn’t, the second one can run when b is less than 1.

So the previous example could be rewritten to take advantage of guards:

int a = A(); // value between 1 and 3
int b = B(); // value between 1 and 3
bool c = HabilitarAvaliacao();
bool resultado = (a, b, c) switch
{
  (_, _, false) => Ok(),
  (_, 3, true) => Ok(),
  (_, _, _) => Nok()
};

Let’s move to a more practical example, where we check the health of a service, and the evaluation takes two arguments: 1) whether the check succeeded and 2) the service’s response time:

var healthCheckResult = (sucesso, tempoRespostaMilissegundos) switch
{
  (true, var ms) when ms <= 5000 => HealthCheckResult.Healthy(),
  (true, _) => HealthCheckResult.Degraded(),
  (false, _) => HealthCheckResult.Unhealthy(),
};

In the scenario above, we have an interesting use of a guard: the service is only considered healthy when the response time is up to 5 seconds.

This is a very compact and concise way to represent several scenarios in expressive, easy-to-read code, and it’s particularly useful for rewriting complex or confusing chains of ifs.

Implementing state machines

This approach is also very useful for describing state machines.

For example, a state machine for an electric gate could look something like this:

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
};

Type deconstruction

One last feature I’d like to show here is type deconstruction, in other words, a structure pattern.

To illustrate, look at these two structs, which represent points in 2D and 3D coordinate systems, respectively:

public struct Point2D
{
  public int X { get; }
  public int Y { get; }

  public Point2D(int x, int y) => (X, Y) = (x, y);

  public void Deconstruct(out int x, out int y) => (x, y) = (X, Y);
}

public struct Point3D
{
  public int X { get; }
  public int Y { get; }
  public int Z { get; }

  public Point3D(int x, int y, int z) => (X, Y, Z) = (x, y, z);

  public void Deconstruct(out int x, out int y, out int z) => (x, y, z) = (X, Y, Z);
}

With the Deconstruct method implemented, we can use pattern matching to determine which quadrant a 2D point is in:

public static string ComputeQuadrant(Point2D point) => point switch
{
  (0, 0) => "Origin",
  var (x, y) when x > 0 && y > 0 => "One",
  var (x, y) when x < 0 && y > 0 => "Two",
  var (x, y) when x < 0 && y < 0 => "Three",
  var (x, y) when x > 0 && y < 0 => "Four",
  var (x, _) when x > 0 => "Positive X axis",
  var (x, _) when x < 0 => "Negative X axis",
  var (_, y) when y > 0 => "Positive Y axis",
  var (_, y) when y < 0 => "Negative Y axis"
};

This is a really interesting example because it shows how we can use pattern matching to deconstruct a custom type and apply several guards to reach a decision.

Conclusion

Pattern matching is an extremely powerful and expressive feature. It can help simplify a lot of complex, hard-to-read code, especially code that involves many conditions and validations.

I hope this article has shown some of the possibilities pattern matching offers, and that you can apply these concepts in your own projects.