A couple of nice Tuple use cases

Posted on Monday, 16th March 2020

C# has had a Tuple type (System.Tuple) for sometime now, however a re-occuring theme was it generally lacked the utility that tuples in languages with native support had. This partly due to the fact that it was really just a class utilising generics with no real language or runtime support, resulting in its adoption being rather limited.

Since C# 7 and the introduction of the ValueTuple type (freeing up some GC cycles) and tuple deconstruction, things seem to have changed and I’ve noticed more and more libraries and developers utilising them, myself included.

With that in mind I’d like to highlight a couple of creative use cases I’ve seen people using that I’d never considered before:

Tuple constructors

I first learned about this pattern at a recent Jon Skeet talk I attended at the .NET South West meet up I co-organise. Jon was demoing some new C# 8 language features and stopped to talk about how he’d adopted this particular pattern.

Given the following constructor we can make this a little shorter without losing much readability using tuples:

// Before
public class Person
{
private readonly string _firstName;
private readonly string _surname;
private readonly int _age;
public Person(string firstName, string surname, int age)
{
_firstName = firstName;
_surname = surname;
_age = age;
}
}
view raw before.cs hosted with ❤ by GitHub
// After
public class Person
{
private readonly string _firstName;
private readonly string _surname;
private readonly int _age;
public Person(string firstName, string surname, int age)
=> (_firstName, _surname, _age) = (firstName, surname, age);
}
view raw after.cs hosted with ❤ by GitHub

When we look at the lowered version of the code using a tool such as SharpLab, we can see that the Tupled version is lowered to exactly the same syntax as the version before which means there’s no cost in using such a pattern.

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)]
[assembly: AssemblyVersion("0.0.0.0")]
[module: UnverifiableCode]
public class Person
{
private readonly string _firstName;
private readonly string _surname;
private readonly int _age;
public Person(string firstName, string surname, int age)
{
_firstName = firstName;
_surname = surname;
_age = age;
}
}
view raw lowered.cs hosted with ❤ by GitHub

Tuple Methods

As you might expect, you can also use the same approach for methods. Again this incurs no additional cost:

public class Person
{
private string _firstName;
private string _surname;
private int _age;
public void SetPerson(string firstName, string surname, int age)
=> (_firstName, _surname, _age) = (firstName, surname, age);
}
view raw person.cs hosted with ❤ by GitHub

Tuple / Deconstructor loops

Another pattern I stumbled upon is using tuples within a loop. For instance given the following dictionary of people, due to KeyValuePair now featuring a Deconstruct method, we can unpack the contents in a few ways.

Traditionally we’d have done this:

var people = new Dictionary<string, Person>
{
["item0"] = new Person {Surname = "Dylan", FirstName = "Bob"},
["item1"] = new Person {FirstName = "Jimi", Surname = "Hendrix"}
};
foreach (var person in people)
{
Console.WriteLine($"{person.Key} {person.Value.FirstName} {person.Value.Surname}");
}

With the cost efficiently of the ValueTuple type we can start to get creative:

foreach (var (key, person) in people)
{
Console.WriteLine($"{key} {person.FirstName} {person.Surname}");
}
view raw tupleloop.cs hosted with ❤ by GitHub

At this point we can go one further by adding a Deconstruct method in our Person class, allowing us to deconstruct the properties into a tuple:

public class Person
{
...
public void Deconstruct(out string firstName, out string surname)
{
firstName = FirstName;
surname = Surname;
}
}
...
foreach (var (key, (firstName, surname)) in people)
{
Console.WriteLine($"{key} {firstName} {surname}");
}
view raw tupleloop1.cs hosted with ❤ by GitHub

Benchmarking the above patterns

For peace of mind, let’s benchmark the aforementioned patterns with BenchmarkDotNet to see what impact it has:

[MemoryDiagnoser]
public class TupleBenchmark
{
private IDictionary<string, Person> _people;
[GlobalSetup]
public void Setup()
{
_people = new Dictionary<string, Person>
{
["person0"] = new Person {FirstName = "Jimi", Surname = "Hendrix"}
};
}
[Benchmark]
public string List()
{
KeyValuePair<string, Person> person = _people.First();
return $"{person.Key} {person.Value.FirstName} {person.Value.Surname}";
}
[Benchmark]
public string DeconstructionToTuple()
{
(string key, (string firstName, string surname)) = _people.First();
return $"{key} {firstName} {surname}";
}
}
|                Method |     Mean |   Error |  StdDev |  Gen 0 | Gen 1 | Gen 2 | Allocated |
|---------------------- |---------:|--------:|--------:|-------:|------:|------:|----------:|
|                  List | 142.9 ns | 2.92 ns | 6.17 ns | 0.0439 |     - |     - |     184 B |
| DeconstructionToTuple | 138.8 ns | 2.21 ns | 2.07 ns | 0.0439 |     - |     - |     184 B |

As you can see they’re pretty much equal in terms of cost, with the added benefit of a more expressive syntax.

Conclusion

Hopefully the patterns highlighted above are as new to you as they were to me, and sparked some more interest in the utility of the ValueTuple type, especially when used in conjunction with KeyValuePair and/or the Deconstruct method. I can only assume we’ll start to see more patterns like this emerge in time as Record types make their way into C#.