C# 7: what's new?

Visual Studio 2017 hit RTM this week, so it's time to take a look at all the new features that made it into C# version 7. There's quite a lot to get through, so let's dive right in...

Tuples

The .NET framework has known tuples through the System.Tuple type since .NET 4. In C# 7, tuples are made an actual language feature. This is done through the new System.ValueTuple type, which is a mutable struct (System.Tuple is a class). This type is not part of the .NET BCL; you'll have to reference the System.ValueTuple NuGet package in order to use it.

Here's the most basic way of using a tuple:

var coords = (12, 6);

Console.WriteLine(coords.Item1);

This looks a lot like the original System.Tuple is used. However, with the new ValueTuple, it's possible to name the items in the tuple:

var coords = (x: 16, y: 97, z: 19);

Console.WriteLine(coords.x);

That's a great improvement in usability and readability.

Tuples, both named and unnamed, can be used anywhere a type can be used. This makes for a great way of having functions return multiple fields, without having to wrap them in a class or struct:

public (string name, int age) GetPerson()
{
    return ("Alice", 20);
}

So, constructing tuples is pretty easy. The other part of the equation is deconstructing, which places the items in the tuple into separate variables. Here's three ways of deconstructing a tuple into new variables:

(string x, int y) = GetPerson();
var u, var v) = GetPerson();
var (w, z) = GetPerson();

Note that tuples are compatible if the types of their fields are compatible. The names themselves don't actually matter. So, for example, you can still assign ints to doubles, as you would expect:

var coords = (4, 2);

(double x, double y) = coords;

Some other interesting tuple-related facts:

  1. You can mix named and unnamed fields:
    var mixedTuple = (name: "Eve", age: 20, "Hacker");
    
    Console.WriteLine($"{mixedTuple.name}, {mixedTuple.age}, {mixedTuple.Item3}");
  2. You can decompose into existing fields or properties:
    var coords = (x: 4, y: 2);
    string z = "Eve";
    
    (z, coords.y) = GetPerson();
    
    public (string name, int age) GetPerson()
    {
        return ("Alice", 24);
    }
  3. When decomposing, you can discard fields you don't need:
    (var name, _) = GetPerson();
    
    public (string name, int age) GetPerson()
    {
        return ("Alice", 24);
    }

Limitations

When deconstructing, you cannot mix assignment and declaration of new variables:

var coords = (x: 4, y: 2);

(coords.x, var z) = (6, 3); // won't compile

Deconstruction

The new tuples aren't the only type that can be deconstructed. In fact, every class can now support the new deconstruction syntax, by implementing a special function:

public class Person
{
    public string Name { get; set; }

    public int Age { get; set; }

    public string Job { get; set; }

    public void Deconstruct(out string name, out int age)
    {
        name = Name;
        age = Age;
    }
}

This let's you do the following:

var person = new Person
{
    Name = "Alice",
    Age = 20
};

var (x, y) = person;

// string x = "Alice"
// int y = 20

You can implement multiple deconstruction functions:

public class Person
{
    // ...
    
    public void Deconstruct(out string name, out int age, out string job)
    {
        name = Name;
        age = Age;
        job = Job;
    }
}

...which let's you do this:

var person = new Person
{
    Name = "Alice",
    Age = 20,
    Job = "Hacker"
};

var (x, y) = person;
var (a, b, c) = person;

Limitations

You can't seem to overload deconstructors. The following class compiles just fine:

public class Person
{
    public string Name { get; set; }

    public int Age { get; set; }

    public string Job { get; set; }

    public void Deconstruct(out string name, out int age)
    {
        name = Name;
        age = Age;
    }

    public void Deconstruct(out string name, out string job)
    {
        name = Name;
        job = Job;
    }
}

...but does not let you use the deconstruction syntax. The following does not compile:

var person = new Person
{
    Name = "Alice",
    Age = 20
};

(string x, int y) = person; // won't compile

Even though the types of x and y are explicitly defined, it still complains that the call is ambiguous between the two destructor functions.

out variables

In C# 7, declaration and assignment of out variables can be combined. If you have simple function with an out parameter:

public void OutVariable(out int answer)
{
    answer = 42;
}

...you can now do this:

OutVariable(out int x);

Console.WriteLine(x);

This will be extremely useful for TryParse functions. In C# 6, you had to do this:

var str = "12";

int x;
if (Int32.TryParse(str, out x))
{
    Console.WriteLine(x);
}

This has always felt rather clunky. Now, in C# 7, you can simply do the following:

var str = "12";

if (Int32.TryParse(str, out int x))
{
    Console.WriteLine(x);
}

What's even more, the use of var is allowed here, so you no longer have to make the type explicit:

var str = "12";

if (Int32.TryParse(str, out var x))
{
    Console.WriteLine(x);
}

"pattern matching"

I'm putting this in sarcasm quotes, because it's not really pattern matching (like you might expect from F#) that we're getting in C# 7. Rather, what we're getting might be more aptly described as type matching.

First of all, the "is" operator can now directly assign a new variable, like so:

object x = 42;
if (x is int y)
{
    Console.WriteLine(y + 10);
}

Secondly, switch statements can now use type matching:

object x = 42;

switch (x)
{
    case string str:
        Console.WriteLine(str);
        break;
    case int i when i > 10:
        Console.WriteLine(i * 2);
        break;
    case int i:
        Console.WriteLine($"{i} is too small...");
        break;
    default:
        break;
}

Note that in our example, the case clauses are not disjoint. In this case, the order matters: they are checked in order, top to bottom, and only the first match is used.

Local functions

In C# 7, you can declare functions inside functions. Here's a minimal example:

public void Foo()
{
    Console.WriteLine(mult(2, 4));

    int mult(int x, int y)
    {
        return x * y;
    }
}

This can be pretty useful for keeping your code clean. For instance, a common refactoring is moving duplicate code to its own function. This removes the duplicate code, but pollutes the class with a helper function that's only ever used in one other function. Now in C# 7, you can instead move duplicate code blocks to a local function, without having to pollute the class.

Here's a slightly more complete example, showing what you can do with local functions:

public void Foo()
{
    sequence(5);

    // simple local function
    int mult(int x, int y)
    {
        return x * y;
    }

    // another local function
    void sequence(int start)
    {
        // reference local function
        write(start);

        if (start > 0)
        {
            // self-references
            sequence(start - 1);
        }

        // functions inside functions
        void write(int x)
        {
            // reference other local functions
            Console.WriteLine(mult(x, 2));
        }
    }
}

As you can see, you can declare functions inside functions inside functions... You can also reference other local functions as you would expect.

ref

C# 7 brings two changes involving the ref keyword. First of all, you can create ref variables:

var x = 9;
ref int y = ref x;

// y = 9

y = 42;

// x = 42
// y = 42

Secondly, you can return things by ref:

public ref int FindMax(int[] values)
{
    var max = 0;
    var pos = 0;

    for (int i = 0; i < values.Length; i++)
    {
        if (values[i] > max)
        {
            max = values[i];
            pos = i;
        }
    }

    return ref values[pos];
}

Which lets you do:

var array = new[] { 1, 8, 3, 9, 5 };
ref int max = ref FindMax(array);

max = 0;

// array = { 1, 8, 3, 0, 5 }

Yay, I guess. I'll be perfectly honest: I don't use the ref keyword, ever. So the use of this stuff is kinda lost on me.

Limitations

After assigning a ref variable, you can't change it to point to something else. So, this won't work:

var x = 9;
ref int y = ref x;

var z = 5;
y = ref z;  // won't compile

Numeric literals

Couple of small changes around numeric literals. First of all, the underscore can be used as a digit separator, like so:

var x = 0xFF_CC_00;
var y = 1_000_000;

Or, if you insist, like so:

var z = 1______0__0;  // this is fine

Next, C# 7 introduces binary literals:

var x = 0b0001; // 1
var y = 0b0010; // 2
var z = 0b0100; // 4

Good thing they introduced those digit separators, otherwise this wouldn't really be readable.

Limitations

You can only place the digit separators between digits. What a surprise, I know. None of the following compiles:

var a = _1_00;
var b = 0x_FF;
var c = 1_00_;

Expression bodies

Constructors, destructors, getters, and setters can all have expression bodies in C# 7. I can't really think of many uses for the *structors, but for getters and setters you can now do something like this:

public class Person
{
    public string FirstName { get; set; }

    public string LastName { get; set; }

    public string FullName { get => $"{FirstName} {LastName}"; }
}

Exceptions in expressions

C# 7 let's you throw exceptions in certain expressions, like so:

string a = null;
string b = a ?? throw new ArgumentNullException();

Combining a couple of other new language features, you can create some pretty interesting constructions:

public class FooBar
{
    Dictionary<int, string> foo = new Dictionary<int, string>();

    public string this[int index]
    {
        get => foo.TryGetValue(index, out var bar) ? bar : throw new ArgumentException();
    }
}

Thoughts

Overall, I like the new features. The new tuples definitely stand out for me as something that will prove very useful in many areas. Everything else is a nice incremental upgrade to an already great language. The only disappointment I have is with pattern matching, or rather, the lack of it. C# is still trailing F# in this area, and it's time this gap got closed. What we have right now will be useful, especially the x is var y expressions, but it's a far cry from what F# has to offer. Fortunately, Microsoft has already stated that they plan to add more pattern matching to C# in the future, so there's already something to look forward to in C# 8!