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:
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
:
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:
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):
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:
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:
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:
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:
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
Post a Comment