C# Unsung Heroes: the Value Tuple

C# v7.0 (introduced with .NET Framework v4.7 in Visual Studio 2017) first introduced the idea of a value tuple to supersede the much more rudimentary System.Tuple class. While System.Tuple is provided by the Standard Library as a family of generic types, value tuples benefit from sophisticated compiler support to provide an efficient and neat syntax for working with tuples. While value tuples aren’t applicable everywhere, their advantages are easily unappreciated. Selective use of value tuples can make your code simpler and more efficient. What’s not to like?

Tuples are useful as simple lightweight types that associate two or more related bits of data together, but don’t warrant a named type of their own. Sometimes it’s useful to create temporary relationships, perhaps to capture just part of another object, or to model an association between objects in a transient way—​say to create a reverse-lookup between types that already have an established relationship. Tuples are admirably suited to such transitory associations, especially where modeling every possible combination with its own named type would add unnecessary complexity.

1. The Basic Mechanics of Tuples

The most obvious way to create a value tuple is to contain multiple values between parentheses, a syntax that matches that in several other languages with built-in tuple support. A simple example of creating a pair—​a tuple of two elements—​is shown here:

Listing 1. New-style value tuple
var pair = (5, "May");

Assert.That(pair.Item1, Is.EqualTo(5));
Assert.That(pair.Item2, Is.EqualTo("May"));

If you’re already familiar with tuple support in, say, F# or Python, this syntax should be familiar. To add a little perspective, here’s the equivalent code using the older System.Tuple:

Listing 2. Old-style "classic" tuple
var pair = Tuple.Create(5, "May");

Assert.That(pair.Item1, Is.EqualTo(5));
Assert.That(pair.Item2, Is.EqualTo("May"));

Both the System.Tuple and newer value tuple can have elements of different types. In these examples, the pair variable is a tuple of an int and a string. In each case we use the Item1 and Item2 properties to access the individual components.

The type of pair in Listing 2 is System.Tuple<int, string>, created using the static Tuple.Create factory method which takes advantage of the type deduction afforded to generic methods to deduce the types of its arguments without having to type arguments for the generic type parameters. Alternatively, we could have used a constructor to create pair, but would have to specify the type arguments explicitly like this:

var pair = new Tuple<int, string>(5, "May");

In a similar way, value tuples are built on the System.ValueTuple underlying type, so the pair in Listing 1 is a System.ValueTuple<int, string>, but the similarity with Listing 2 ends there. We create the pair value tuple simply by enclosing two values within parentheses, rather than via a Create method. The differences from System.Tuple don’t end there.

2. The Value of Names

The ItemN properties used to access the individual components aren’t a major problem, but they do require us to remember what they mean. In a proper type we’d try to give sensible names to properties that represent the purpose of the property. The old-style tuple type doesn’t allow us to do anything like that, but value tuples do allow us to give names to each component value:

Listing 3. Naming tuple components
var pair = (Month: 5, Name: "May");
 
Assert.That(pair.Month, Is.EqualTo(5));
Assert.That(pair.Name, Is.EqualTo("May"));

The names we’ve given here to the values being assigned to the pair tuple can later be used to access the individual component parts. This is one example of the benefits of true compiler support enjoyed by value tuples, because even though—​behind the scenes—​the pair variable is an instance of ValueTuple<int, string>, that underlying type has no knowledge of Month or Name. In fact, those names never even make it into the compiled code; the compiler translates them to Item1 and Item2 respectively. The names we give the component parts of a tuple are for our convenience. At run time, Listing 3 is identical to Listing 1.

3. Deconstruction

You may already be familiar with the term deconstruction even if you’re not so familiar with value tuples because record types support the same syntax. The basic idea is demonstrated here (but stay with me, because this example doesn’t really do it justice):

Listing 4. Deconstructing tuple components
var (month, name) = (5, "May");
 
Assert.That(month, Is.EqualTo(5));
Assert.That(name, Is.EqualTo("May"));

Here the only tuple is the source value (5, "May") whose component parts are assigned to two separate variables named month and name. This is called deconstruction because we’re essentially breaking a tuple down into its component parts.

Deconstruction is built-in to many of the tuple-like types in the Standard Library. A prime example is the KeyValuePair<TKey, TValue> type, perhaps most recognizable as the element type of a Dictionary<Tkey, TValue>.

var occupants = new Dictionary<Room, Guest>();

foreach (var itemPair in occupants)
{
    var (room, guest) = itemPair;
    // ...
}

Here the itemPair variable is a KeyValuePair<Room, Guest> type, the element type of the occupants dictionary. Rather than using itemPair.Key and itemPair.Value we deconstruct the object into variables with more meaningful names. We can be even briefer by deconstructing the iteration variable in-place like this:

foreach (var (room, guest) in occupants)
{
}

This technique is useful in a number of places, such as in a constructor:

public class Guest
{
    public Guest(string name, Address address, CardDetails card)
        => (FullName, Address, PaymentCard) = (name, address, card);
    
    public string FullName { get; }
    public Address Address { get; }
    public CardDetails PaymentCard { get; }
}

Here we’re deliberately creating a tuple from the three parameters to Guest’s constructor so that it can be deconstructed into the properties. The compiler translates this code into an efficient assignment of each value to the backing field for each property. Note that if any of the properties had a set or init accessor, the translated code would set the property via that accessor, instead of just assigning to the backing field directly. However, the efficiency implications of that are tiny, and could probably be optimized away in any case.

Deconstruction, and value tuples generally, plays an important role in pattern matching, which we’ll perhaps leave for another post.

4. Value Semantics

Value tuples are true value types—​the ValueTuple type underlying all value tuples is a struct. By contrast, the old-style System.Tuple was a class. This difference has important behavioural consequences. Consider this code:

Listing 5. Value tuples have value semantics
var red = (Red: 0xFF, Green: 0, Blue: 0);

var yellow = red;
yellow.Green = 0xFF;

Assert.That(yellow.Green, Is.EqualTo(0xFF));
Assert.That(red.Green, Is.EqualTo(0));

Here we assign red to yellow, and modify one of the components of the target yellow variable. As a value type, the value of red is copied by value into yellow, meaning that yellow has its own independent copy of the entire value from red. When we copy yellow, those changes aren’t reflected in red.

This example might—​rightly—​be raising questions about mutability; it’s strongly encouraged to make value types immutable to avoid some common pitfalls and make code easier to follow. Value tuples are indeed mutable, and in fact the components of a value tuple are public fields, contravening another common guideline regarding type design. For a complete discussion as to why, see Tuple Trouble: Why C# Tuples Get to Break the Guidelines. Suffice to say here that neither infringement of common rules is a cause for real concern.

In any case, in C# v10.0 and later we can write the code in Listing 5 more simply because value tuples support Nondestructive mutation using the with keyword, allowing us to change the assignment and subsequent change in yellow with the following:

var yellow = red with { Green = 0xFF };

One crucial aspect of value semantics concerns value-based equality comparisons, so value tuples naturally support the concept that two tuples are "equal" if their component fields all compare equal. The old System.Tuple type supported this when calling the Equals method, but not when comparing values using the == operator. Value tuples can be compared with ==, and this can be used to good effect when writing Equals for our own types, as shown in the type-safe Equals method here:

Listing 6. Value-based equality for value tuples
public readonly struct Color : IEquatable<Color>
{
    public Color(int r, int g, int b)
        => (Red, Green, Blue) = (r, g, b);
    
    public int Red { get; init; }
    public int Green { get; init; }
    public int Blue { get; init; }

    
    public bool Equals(Color other) 
        => (Red, Green, Blue) == (other.Red, other.Green, other.Blue);

    public override int GetHashCode() 
        => (Red, Green, Blue).GetHashCode();
    
    
    public override bool Equals(object? obj) 
        => obj is Color other && Equals(other);
}

The comparison with == for value tuples works by lifting the operator== implementation of the individual components (Eric Lippert describes operator lifting nicely here if you’re not familiar with the idea). We can call the Equals method of the value tuple variable, too, but that will create a new tuple instance for the argument, then pass it by value as an argument. Comparing value tuples using == is therefore more efficient, but requires all the component types to be comparable using ==; if any of those components has no accessible operator==, then Equals is our only option.

Value tuples also implement GetHashCode to create well-distributed hash values based on the components of the tuple (shown in Listing 6 in the GetHashCode override). In this instance, however, we’re probably better off writing our own like this:

public override int GetHashCode() 
    => HashCode.Combine(Red, Green, Blue);

Calling HashCode.Combine is more direct, and is what the implementation of GetHashCode in ValueTuple does in any case.

5. Finally

I have one last trick up my sleeve. It’s sometimes necessary to swap the values of two variables. The traditional method is to swap them via a temporary value, like this:

Listing 7. Traditional swap implementation
var foreground = new Color(0xFF, 0, 0);
var background = new Color(0, 0, 0xFF);

var temp = foreground;
foreground = background;
background = temp;

Value tuples, in conjunction with deconstruction, make swapping two values trivial:

Listing 8. Swap using value tuples
var foreground = new Color(0xFF, 0, 0);
var background = new Color(0, 0, 0xFF);

(foreground, background) = (background, foreground);

Assert.That(foreground.Blue, Is.Not.Zero);
Assert.That(background.Blue, Is.Zero);

The second method of swap using value tuples isn’t just neater in the code, it’s much more direct. Moreover, the version in Listing 7 would normally indicate a separate Swap method with a pair of out parameters, or an instance method on Color, also with an output parameter for the result. The single-line approach in Listing 8 almost certainly makes the separate method implementation redundant.

There’s much more to value tuples for you to discover, but I hope this post has inspired you to make more use of this unappreciated feature of modern C#.


Photo by Alex Padurariu on Unsplash

Comments

Popular Posts