Friday Links 0.0.25 - C#-7: Pattern Matching (Lite)

This is based on an email I send my .NET team at work

Happy Friday,

Continuing our tour of new features in C#-7, today we come to Pattern Matching.

Pattern matching is a feature of many programming languages that let you test if the contents of a variable meet a certain shape, or pattern. So you can test if a variable is of a class, or in some languages, if a variable has certain properties.

If you’re familiar with the more robust pattern-matching capabilities of a language like F#, you’d be disappointed with what’s being introduced here, but this feature is only going to get more powerful and expressive over time.

There’s two new constructs in C#-7 that support pattern matching.

Is-Expressions

It’s really common in certain data models and class hierarchies to need to test what particular type you have, cast it, and then work with the casted result. For example, imagine this data model which I’ve extracted from a side project:

Results.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class HttpResult
{
public int StatusCode{ get; set; }
}

public class RedirectResult : HttpResult
{
public string Location{ get; set; }
}

public class HtmlResult : HttpResult
{
public string HtmlBody { get; set; }
}

Processing these results often involves determining what type of result you have, then casting and accessing its properties.

Process.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public void Process(HttpResult result)
{
// one way is to use is to check, then cast
if (result is RedirectResult)
{
// now cast
var location = ((RedirectResult)result).Location;
Console.WriteLine($"Redirected to ${location}");
return;
}

// another way is to use "as" and check for null
var htmlResult = result as HtmlResult;
if (htmlResult != null)
{
Console.WriteLine($"The contents was ${htmlResult.HtmlBody.Length} characters long.");
}
}

The new C# pattern matching feature lets you simplify this a little bit. An is expression can also provide a variable name, and if the type matches, the variable name comes in scope and has the casted value.

ProcessCSharp7.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public void Process(HttpResult result)
{
if (result is RedirectResult redirect)
{
// redirect variable in scope here
Console.WriteLine($"Redirected to ${redirect.Location}");
return;
}

// redirect variable is in scope, but unassigned

if (result is HtmlResult htmlResult)
{
Console.WriteLine($"The contents was ${htmlResult.HtmlBody.Length} characters long.");
}
}

A little bit cleaner, right?

The variable declared in the is expression is in scope following the is expression, but is considered unassigned. Consider:

1
2
3
4
5
6
7
8
9
10
11
object o = 5; // stick an int into an object reference

if (o is int i)
{
Console.WriteLine(i); // i is in scope and assigned
}

Console.Writeline(i); // ERROR CS0165 Use of unassigned local variable 'i'

i = 20; // i is in scope and can be assigned
Console.WriteLine(20);

I like this feature for reducing some of the extra noise that gets in the way of understanding whats going on in a function.

Pattern Matching in Switch Statements

They’ve also added support for pattern matching in switch statements, with the additional ability to filter on properties of the matched pattern.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public void ProcessWithSwitch(HttpResult result)
{
switch (result) // can now switch on any object
{
// Just like an is expression, we can declare a variable
case HtmlResult html:
Console.WriteLine($"Is HTML with {html.HtmlBody.Length} characters");
break;

// We can also declare a variable AND do checks on it
case RedirectResult r when r.StatusCode == 301:
Console.WriteLine($"Is permanent redirect to {r.Location}");
break;
case RedirectResult r when r.StatusCode == 302:
Console.WriteLine($"Is temporary redirect to ${r.Location}");
break;

// case null now supported
case null:
throw new ArgumentNullException(nameof(result));
break;

default:
Console.WriteLine($"How'd you get this?");
break;
}
}

Once again, ReSharper puts red squiggles under everything, but it does still compile.

Unlike the is expressions, variables declared in the case statement are only in scope for that case. You can’t access r outside of the switch or in another case block.

I don’t tend to use switch statements all that often, but its nice that concepts you can use in if blocks can also apply in switches.

Here’s the proposal for Pattern Matching on GitHub: https://github.com/dotnet/roslyn/blob/features/patterns/docs/features/patterns.md

There are a number of features of this proposal not yet implemented, that will hopefully come in future versions of C#.