What's New in C# - Pattern Matching and Local Functions

What’s New in C# 7.0? – Pattern Matching and Local Functions

C# 7.0, the latest major version of the exceptionally popular language, was released in March 2017 alongside Visual Studio 2017, bringing a number of new features and capabilities to the table. Today we’ll dive deeper into the pattern matching and local functions features in our ongoing series, What’s New in C# 7.0?:

  • In Part 1 we thoroughly explored tuple types, tuple literals, and out variables.

Let’s get to it!

Pattern Matching

One handy feature that C# 7.0 brings to the table is patterns, which provide a simple syntax to test whether an object meets a criteria related to its value or type (for now). As of writing, there are currently three different pattern matching types, but the C# team has promised that additional pattern types will be introduced in the future.

  • Constant patterns are quite standard and something we’ve seen before. These effectively test if an input is equal to a particular constant value.
  • Type patterns check if an input has a particular type, and if so, extracts the input value into a new variable of that type.
  • Var patterns don’t perform a conditional match of any kind, thereby making them always match. The purpose of a var pattern is to generate a new variable with the value and type of the input.

These will make far more sense in code, so let’s jump right into the code sample. We start with a series of classes inheriting from one another, all based on the IOrganism interface:

These classes don’t do much on their own, other than estimate their respective global populations (the stats of which were acquired from this publication). However, we’ll use these classes to illustrate the differences (and potential uses) of the various pattern matching types.

Type Patterns

We start with the GetPopulationUsingType(IOrganism organism) method, which uses a switch case in combination with the new type pattern to make it easy for us to differentiate between all the potential types that could be passed into this method:

Since IOrganism is the baseline interface, and the Mammal and Insect classes implement that interface, which are, in turn, inherited by Human and Bee, it would typically be somewhat challenging to properly differentiate between these types using a series of if-else statements. However, with type patterns, we can simply use a switch (organism) statement, and then create a type-specific case statement that will check if the passed type matches. We can go even further and apply a bit of filtering, which we’ve done with the two case Human human statements. In the event that a Human object is passed, if the Population is too low our first case Human human matches and executes, otherwise the second case Human human will do so.

To test this out we’ll call this method a few times by passing different organism types:

The resulting output from this looks expected — the populations are output for the first four, but the low human population output on the final call causes a problem:

Var Patterns

To see the var pattern in action we’ve created a GetPopulationUsingVar(IOrganism organism) method:

This method also accepts an IOrganism instance and uses a switch statement to handle logic based on the type that was passed. However, notice the syntax of our case statements. By using case var bee in the first case statement we’ve implemented a var pattern. This extracts a matched value of organism and assigns it to the newly-created bee variable, which is then a local variable we can use within the case statement scope. Thus, our first case statement tries to capture a passed Bee object by checking that the population is roughly equal to what we expect for bees, while the human case statement performs a check of the type Name property.

We can test these out and verify everything works as expected in the VarPatternExample() method:

Executing this results in the first two calls working fine, however, the Mammal passed to the final call is an unknown type, so we get a different output informing us the method doesn’t know how to handle it (couldn’t find a match):

Constant Pattern and Is-Expressions

The last pattern to look at is the constant pattern. Since such a pattern is very basic, we’ll also explore it along with the is-expression, which allows us to check if an object is equivalent to a particular value, or is of a particular type (via a type pattern). The GetPopulationUsingIs(object organism) method accepts an object, so we can test for the null constant using a constant pattern via an is-expression. We also check if the passed object is of the type IOrganism. If so, we assign it to the new variable of o, which we use for the output:

To test this method out we’ll use IsExpressionPatternExample(), which passes a couple objects that both inherit from IOrganism, followed by passing null:

The output from running this method is what we expected — population data for Insect and Mammal, followed by a cancellation message when passing null:

Local Functions

Another cool feature C# 7.0 adds is the ability to create local functions. A local function is, as the name implies, a function that is embedded directly inside the scope of another method. To see this in action, we’ll start with the full code snippet, then break it down afterward:

We begin with a basic IBook interface and Book class that implements IBook. We’ll use these to create a simple collection of books in just a moment. However, first we need a reason to use a local function within another method. Creating an iterator method is a common scenario in which a local function may prove useful. An iterator typically performs some action upon each element in the collection, then calls a yield statement in order to yield the next element in the collection.

For example, let’s look closer at the Filter<T>(IEnumerable<T> source, Func<T, bool> filter, bool inclusive = true) method:

As the name suggests, its purpose is to filter an enumerable collection using the passed Func, which should return a boolean indicating if the element passed or failed the filtration process. This is a perfect scenario to use a local function, which is exactly what we’ve done with the Iterator() local function found inside. Iterator() just loops through the elements of source and checks if each element passes the filter check, thereby determingin if the element should be yielded. The entire collection of filtered, yielded elements is bubbled up from the Iterator() local function to the return Iterator() statement of the Filter<T>(IEnumerable<T> source, Func<T, bool> filter, bool inclusive = true) method.

The advantage to using a local function here, as opposed to an internal or private method, is that we may not want the local Iterator() function to be available to other members of the parent class. A local function makes it easy to maintain the exact scope level that is required, without exposing any of the functionality to outside callers.

To test this out and make sure it works as expected, we’ll start by creating a Book collection:

This produces the output of all our initial books, as expected:

Now we’ll try filtering our collection using the Filter<T>(IEnumerable<T> source, Func<T, bool> filter, bool inclusive = true) method. We’re also using lambda syntax to simplify the passing of our filter function argument, since we only need to return the result of a single statement. In this case, we’re just checking if the PageCount for each book exceeds 400:

Our original collection contained three books with high page counts, and our output confirms that the filter behaves as desired:

Lastly, to illustrate how we can further alter the behavior of our inner local function, we also added the bool inclusive parameter to the Filter<T>(IEnumerable<T> source, Func<T, bool> filter, bool inclusive = true) method. This allows us to effectively inverse the behavior of the filter process, so any element that would return true from the filter now returns false (and, therefore, is excluded):

Here we should see the opposite result of our previous filtration in the output:

Stay tuned for future parts in this series where we’ll continue exploring the new features introduced in C# 7.0! And don’t forget, the Sharpbrake library provides robust exception tracking capabilities for all of your C# and .NET applications. Sharpbrake provides real-time error monitoring and automatic exception reporting across your entire project, so you and your team are immediately alerted to even the smallest hiccup, and can appropriately respond before major problems arise. With a robust API and tight integration with the powerful Airbrake web dashboard, Sharpbrake will revolutionize how your team manages exceptions.

Check out all the great features Sharpbrake brings to the table and see why so many of the world’s top development teams use Airbrake to dramatically improve their exception handling practices!