Rob West

C# Tuples as Syntactic Sugar for Multiple Variable Assignment

Published 12 Jun 2020

I came across an interesting pattern for using Tuples in an article by Mads Torgersen on C# 9.0. At first I thought it must be something new in 9.0, but a little digging and actually engaging my brain made me realise it has been possible since C# 7.0 and the introduction of Tuples. It isn't something I've seen anybody else use so I thought it would be worth writing about. 

I have to admit that I've not really looked at Tuples that much and learning a bit more about them reveals some features that allow a style of assignment through destructuring that is ubiquitous in modern JavaScript.

Here is the bit of code from Mads' article that triggered my curiosity (expression bodies wrapped by me for legibility online):

public data class Person 
{ 
    string FirstName; 
    string LastName; 
    public Person(string firstName, string lastName)  
        => (FirstName, LastName) = (firstName, lastName);
    public void Deconstruct(out string firstName, out string lastName) 
        => (firstName, lastName) = (FirstName, LastName);
}

At this point in his article he is actually explaining records using the data keyword (very cool). These feature new init-only properties and are intended as value-like immutable data. He offers the above example as a positional approach to record creation and usage. I have to admit I'd not known about the Deconstruct method which assigns values to any number of out variables. We'll get to that below, but the bit that initially caught my eye was the constructor. Both properties are wrapped in parentheses, as are the two constructor parameters, so the assignments are both done in one line. So what's going on here?

The answer, as I've alluded to above, is Tuple deconstruction. The normal way that you consume tuples as return types is to create the variables in the expression:

private static (double, double, int) ComputeSumAndSumOfSquares(IEnumerable<double> sequence)
{
    // method body omitted
}

// you can explicitly declare the types of each field
(int count, double sum, double sumOfSquares) = ComputeSumAndSumOfSquares(sequence);

// or you can implicitly type variables for all fields by placing var outside the brackets
var (sum, sumOfSquares, count) = ComputeSumAndSumOfSquares(sequence);

The bit that I was not aware of is tucked away at the end of the section on deconstruction in the tuple docs, where it says that you can deconstruct tuples with existing declarations as well, which is what Mads has done. So the code creates a tuple of the existing declared variables, and one from the arguments and assigns values from one to the other.

As a side note, it is worth pointing out that you cannot mix existing declarations with declarations inside the parentheses, it is one or the other.

Why is this a useful pattern to consider? The answer is brevity and readability. Let's look at an example with a simple class and constructor. The standard approach would look like this:

public class Query
{
    public string CodeName { get; }
    public int Page { get; }

    public Query(string codeName, int page)
    {
        CodeName = codeName;
        Page = page;
    }
}

with the destructuring syntax this becomes:

public class Query
{
    public string CodeName { get; }
    public int Page { get; }

    public Query(string codeName, int page)
    {
        (CodeName, Page) = (codeName, page);
    }
}

and now we have a shorter single line expression we can also use that other bit of syntactic sugar, the expression body, so we can get the whole constructor on one line:

public class Query
{
    public string CodeName { get; }
    public int Page { get; }

    public Query(string codeName, int page) => (CodeName, Page) = (codeName, page);
}

Many of the improvements that have been applied to C# over the years have been about the reduction in the amount of boilerplate code you need to write, what Jon Skeet refers to as "ceremony". It is about getting the compiler to do the work for you so you can get on with the task at hand. Now you always have to be wary of when terseness turns into obscurity. However, in this case I would argue that this approach is actually more easy to read and comprehend than the traditional approach - it is quite expressive of the intent which is why I like it as a pattern.

JavaScript Style Destructuring

I promised to get back to the Deconstruct method, as this can bring a similar reduction in ceremony, that makes some code that looks very JavaScript-like. As a reminder, Mads' example featured this line:

public void Deconstruct(out string firstName, out string lastName) 
    => (firstName, lastName) = (FirstName, LastName);

The power of this method is that it can be used to assign variables like this:

var p = new Person("Peter", "Sagan");
var (firstName, lastName) = p;

This is why I say it is a strong parallel to JavaScript object destructuring where you would see something like this:

const {first, last} = this.props;

Where this gets really interesting is that you can apply a Deconstruct method to types you have not created via extension methods, so you can use this brief destructuring syntax wherever you want like this:

public static class Extensions
{
    public static void Deconstruct(this Person p, out string first, out string last)
    {
        (firstName, lastName) = (FirstName, LastName);
    }
}

You can create multiple Deconstruct methods with different out parameters, but this could get confusing and create more work than it saves. As an alternative you can have single/fewer methods and use Discards, another C# 7.0 feature that I've not used. This uses the underscore to assign a temporary dummy variable that is never used by your application. You use it like this:

var c = new Cyclist("Fabian", "Cancellara", "Leopard Trek", 400);
var (firstName, lastName, _, threshold) = c;

 I've never really spent much time exploring tuples because I've preferred to instantiate return values as types. I've not found situations where those types felt like an overhead. I'm not sure I've changed my stance on that after spending a bit more time investigating them, but this ability to add a Deconstruct extension to a class to enable destructuring assignment is interesting and I'm going to be looking out for opportunities to use this approach to see how it works.

© 2024 Rob West. All Rights Reserved. Built using Kontent and Gatsby.