C# 7 introduces a number of great features, including (but not limited to) pattern matching, tuples, and local functions. Additionally, several existing features and overall performance have been improved, with an eye towards code simplification and clarity.

In this article, I'll go over the new and improved language features and look at ways to take advantage of performance improvements in C# 7 and Visual Studio 2017.

New Features

I'll start by introducing the new features before moving on to the improved stuff.

Tuples

Tuples are a mainstay of F#, and are so incredibly useful that C# and VB developers have been asking for them for some time. As of C# 7, they're finally a part of all three languages.

Essentially, tuples are a way to define simple structures that contain multiple data elements. You could use a class or a struct for this, but sometimes the effort to do so outweighs the benefit. Take the following example:

var FullName = ("Chris", "Williams");

This is referred to as an unnamed tuple. The only way you can refer to the contents of an unnamed tuple is via the fields Item1, Item2, etc. The alternative to this is to initialize your tuples, like so:

var FullName = (FName: "Chris",
                LName: "Williams");

This creates synonyms for the field names, allowing you to refer to the internal values with more meaningful names, as well as still being able to use Item1 and Item2 (although why you'd want to do that, given the better alternative, is beyond me.)

Despite what the previous examples would have you believe, you aren't limited to just two variables in a tuple. Also, under the hood, a tuple is essentially just a struct, and you can handle assignment pretty easily, as in the following example:

var HisName = (FName: "Chris",
               LName: "Williams");
var HerName = (FName: "Maime",
               LName: "Gonzales");
HerName.LName = HisName.LName;
//       Chris got married recently...

You can also do straight assignments, like HisName = HerName, but the types and number of fields must match up or you'll get a “cannot assign” error at compile time.

Tuples are great as return values from methods, and still don't require defining any external classes or structures in your receiving code. You can deconstruct them pretty easily too, to get discrete variables for each field. Assume that the GetHisName() method in the example below returns a tuple containing two strings.

(string FName, string LName) = GetHisName();

If you prefer implicit typing, that works too:

(var FName, LName) = GetHisName();

Tuples, named or unnamed, make it significantly easier to work with statically typed data structures that contain multiple fields, but don't require any of the behaviors of classes and structs. Tuples work best with private and internal methods. Stick to user defined classes or structs for your public methods.

Tuples work best with private and internal methods. Stick to user defined classes or structs for your public APIs.

Pattern Matching

Pattern matching allows you to implement method dispatch on something other than the object's type. For example, when you inherit a class and overload one of its methods, the dispatcher knows which method to call based on the type of the object calling it. Pattern matching expressions allow you to implement similar dispatching rules for elements not related via inheritance. That's a bit of a mouthful, so bear with me and I'll give you some practical examples shortly.

Pattern matching in C# 7 improves upon two previously existing language constructs: is expressions and switch statements.

Prior to C# 7, the is expression only supported querying the type of the object to its right, like so:

if (obj is null) ...
if (obj is string) ...

With pattern matching, you have the ability to add a scoped variable to the type checking and use it, if it passes the test:

if (obj is string s)
     WriteLine(s);

Maybe you have a set of data elements that contain a mixture of single and multiple value entries:

foreach(var item in data)
{
    if (item is int val)
        WriteLine(val);
    else if (item is IEnumerable<object> list)
        foreach (subItem in list)
        {
            if (subItem is int val)
                WriteLine(val);
        }
}        

As you continue to add more and more if statements, you'll quickly get to a point where you're better off with a switch statement, which brings us to the other thing that supports pattern matching.

This is how you'd write the previous example as a switch pattern expression:

foreach(var item in data)
{
    switch (item)
    {
        case int val:
            WriteLine(val);
            break;

        case IEnumerable<object> list:
            foreach (subItem in list)
            {
                if (subItem is int val)
                    WriteLine(val);
            }
            break;
    }
}

Switch expressions can also handle constants, such as checking for 0 or null, if you wanted to exclude them from your set.

When dealing with switch expressions, it's important to note that the order of case statements matters. You need to put the more restrictive cases before the less restrictive ones (such as checking for 0 before checking for int, because a 0 also evaluates as an integer.)

When dealing with switch expressions, it's important to note that the order of case statements matters.

Ref Locals and the Ref Return

You've always been able to pass in values by reference (via the ref keyword), but now you can also store them by reference locally, and return them by reference as well.

This is very handy when working with large data structures, such as a matrix or map array populated by structs that contain information about each location on the grid (video games, anyone?). You can now return a reference to the location of the data, rather than the data itself. This allows you to read and modify it very efficiently, without having to copy values or perform repeated costly de-referencing operations.

In this example, there's an item ID that you need to find within an array of items. This method returns a reference to the location of that item in your items array, so that you can quickly and easily modify it.

static ref int GetItem(int ID, int[,] items)
{
    for (int item=0; item < items.Length; item++)
    {
        if (items[item] == ID)
        {
            return ref items[item];
        }
    }
}
// You can also store the result in a local ref
ref int item = ref GetItem(237, arrItems);

There are some safety restrictions to be aware of:

  • You can only return refs that were passed to you or that point into object fields.
  • Ref locals are immutable, and can't be changed to point to a new storage location.
  • You can't assign a value to a ref variable.

Local Functions

It's common practice to write private class methods that you know will only ever be called once, in an effort to keep each method clean and concise. Unfortunately, too many of these can make your class difficult to decipher by other members of your team (or even when YOU come back after working on something else for a few months).

Local functions are methods declared inside other methods. This makes it immediately obvious that the local method isn't called from anywhere else. It may seem counter-intuitive to start cluttering up your nice, readable methods with single use methods again, but there are actually some pretty compelling use cases for this feature: iterators and async methods.

The following async example validates arguments prior to starting the asynchronous work.

Task<int> UpdateSkill(string skill, int level)
{
    if (string.IsNullOrWhiteSpace(skill))
        throw new ArgumentException
            (message: "Skill name is required.");
    if (level < 1)
        throw new ArgumentOutOfRangeException
            (paramName: nameof(level), 
            message: "Level must be > 0.");
    return AsyncSkillUpdate();

    async Task<int> AsyncSkillUpdate()
    {
        var result = await ApplyLevel();
        return result.ID;
    }
}

In addition to all of the brand new features introduced by C# 7, there are a number of existing features that have had new functionality added in this version.

Improved Features

Now I'll cover the features that already existed in some form but have been improved.

Expression-Bodied Members

C# 6 gave us Expression-Bodied Members that allow you to incorporate an expression directly into a method declaration or read-only property definition.

In C# 7, that list gets significantly expanded to include constructors and finalizers, and get and set accessors on properties and indexers.

Throw Expressions

In addition to the usual Throw() statement, C# 7 introduces the ability to use Throw() as an expression, thanks in part to expression-bodied members. The syntax is unchanged, the only thing that's new is where you can add them, such as conditional expressions:

phone = obj.phone ??
    throw new ArgumentNullException();

Numeric Literals

Numeric literals have been around for a while, but C# 7 adds some little improvements in the form of binary literals and digit separators. If you've ever created a bit mask, you know how hard on the eyes it can be to read a long string of zeros and ones.

Prefix your binary literal with 0b, like so:

public const int mask 0b001011011001

// Make it more readable with a digit separator
public const int mask 0b0010_1101_1001

The digit separators aren't limited to use with binary literals either. You can also use them with integers, decimals, floats, and doubles too.

Out Parameters and Out Variables

If you need to return multiple values from a method call, you can create a custom class as the return type, or you can use the out keyword. Using the out keyword causes arguments to be passed by reference. Out functions like the ref keyword, except when using out, you no longer have to initialize variables before they are passed in.

When using an out parameter, your code looks like this:

class OutParamDemo
{
    static void Main()
    {
        string value;
        GetCustomer(out value);

        // value = Chris
        Console.WriteLine(value);
    }
    static void GetCustomer(out string name)
    {
        name = "Chris";
    }
}

In order to initialize the value, the compiler has to know what to expect back from the GetCustomer() method, so you can't just use var to create it. Also, although you can overload methods containing an out parameter, you can't do so if ref and out are the sole qualifiers.

The ref and out keywords aren't considered part of the method signature at compile time, and can't be the sole qualifier for overloading.

But, now in C# 7, using an out variable, you can do this:

class OutVariableDemo
{
    static void Main()
    {
        GetCustomer(out string value);

        // value = Chris
        Console.WriteLine(value);
    }
    static void GetCustomer(out string name)
    {
        name = "Chris";
    }
}

It's a little cleaner, and because the out variable is declared as an argument to the out parameter, the compiler is smart enough to infer the type automatically, so you could just use var to declare it, if that's your preference.

In the OutVariableDemo() example, the value variable is scoped to the enclosing block, so you can use it anywhere within the Main() sub.

Out Wildcards

C# 7 also introduces wildcards as out parameters. These allow you to safely ignore any return parameters that you don't care about, like so:

GetCustomer(out string value, out *);
// value = Chris, other out values are ignored

Filtering out anything you don't plan to use helps to keep potential conflicts to a minimum and keeps your code clean.

At the time of this writing (C# 7 Preview 4) wildcard out parameters are not yet implemented in C# 7, but are on the roadmap.

At the time of this writing, wildcard out parameters are't yet implemented in C# 7, but are on the roadmap.

That wraps it up. I hope you've enjoyed this overview of the new and improved features of C# 7.