C# 7 ValueTuple types and their limitations
Posted on Thursday, 20th April 2017
Having been experimenting and reading lots about C# 7’s tuples recently I thought I’d summarise some of the more interesting aspects about this new C# language feature whilst highlighting some areas and limitations that may not be apparent when first using them.
Hopefully by the end of this post you’ll have a firm understanding of the new feature and how it differs in comparison to the Tuple type introduced in .NET 4.
A quick look at tuple types as a language feature
Prior to C# 7, .NET’s tuples were an awkward somewhat retrofitted approach to what’s a powerful language feature. As a result you don’t see them being used as much as they are in other language like Python or to some extend Go (which doesn’t support Tuples, but has many features they provide such as multiple return values) - with this in mind it behoves me to briefly explain what Tuples are and why you’d use them for those that may not have touched them before.
So what are Tuples and where you would use them?
The tuples type’s main strengths lie in allowing you to group types into a closely related data structure (much like creating a class to represent more than one value), this means they’re particularly useful in cases such as returning more than one value from a method, for instance:
public class ValidationResult {
public string Message { get; set; }
public bool IsValid { get; set; }
}
var result = ValidateX(x);
if (!result.IsValid)
{
Logger.Log(Error, result.Message);
}
Whilst there’s nothing wrong with this example, sometimes we don’t want to have to create a type to represent a set of data - we want types to work for us, not against us; this is where the tuple type’s utility is.
In fact, in a languages such as Go (which allow multiple return types from a function) we see such a pattern used extensively throughout their standard library.
bytes, err := ioutil.ReadFile("file.json")
if err != nil {
log.Fatal(err)
}
Multiple return types can also stop you from needing to use the convoluted TryParse
method pattern with out
parameters.
Now we’ve got that covered and we’re all on the same page, let’s continue.
In the beginning there was System.Tuple
Verbosity
Back in .NET 4 we saw the appearance of the System.Tuple<T>
class which introduced a verbose and somewhat awkward API:
var person = new Tuple<string, int>("John Smith", 43);
Console.WriteLine($"Name: {person.Item1}, Age {person.Item2}");
// Output: Name: John Smith, Age: 43
Alternatively there’s a static factory method that cleared things up a bit:
var person = Tuple.Create("John Smith", 43);
But there was still room for improvement such as:
No named elements
One of the weaknesses of the System.Tuple
type is that you have to refer to your elements as Item1
, Item2
etc instead of by their ‘named’ version (allowing you to unpack a tuple and reference the properties directly) like you can in Python:
name, age = person_tuple
print name
Garbage collection pressure
In addition the System.Tuple
type is a reference type, meaning you pay the penalty of a heap allocation, thus increasing pressure on the garbage collector.
public class Tuple<T1> : IStructuralEquatable, IStructuralComparable, IComparable
Nonetheless the System.Tuple
type scratched an itch and solved a problem, especially if you owned the API.
C# 7 Tuples to the rescue
With the introduction of the System.ValueTuple
type in C# 7, a lot of these problems have been solved (it’s worth mentioning that if you want to use or play with the new Tuple type you’re going to need to download the following NuGet package.
Now in C# 7 you can do such things as:
Tuple literals
// This is awesome and really clears things up; we can even directly reference the named value!
var person = (Name: "John Smith", Age: 43);
Console.WriteLine(person.Name); // John Smith
Tuple (multiple) return types
(string, int) GetPerson()
{
var name = "John Smith";
var age = 32;
return (name, age);
}
var person = GetPerson();
Console.WriteLine(person.Item1); // John Smith
Even named Tuple return types!
(string Name, int Age) GetPerson()
{
var name = "John Smith";
var age = 32;
return (name, age);
}
var person = GetPerson();
Console.WriteLine(person.Name); // John Smith
If that wasn’t enough you can also deconstruct types:
public class Person
{
public string Name => "John Smith";
public int Age => 43;
public void Deconstruct(out string name, out int age)
{
name = Name;
age = Age;
}
}
...
var person = new Person();
var (name, age) = person;
Console.WriteLine(name); // John Smith
As you can see the System.ValueTuple
greatly improves on the older version, allowing you to write far more declarative and succinct code.
It’s a value type, baby!
In addition (if the name hadn’t given it away!) C# 7’s Tuple type is now a value type, meaning there’s no heap allocation and one less de-allocation to worry about when compacting the GC heap. This means the ValueTuple
can be used in the more performance critical code.
Now going back to our original example where we created a type to represent the return value of our validation method, we can delete that type (because deleting code is always a great feeling) and clean things up a bit:
var (message, isValid) = ValidateX(x);
if (!isvalid)
{
Logger.Log(Log.Error, message);
}
Much better! We’ve now got the same code without the need to create a separate type just to represent our return value.
C# 7 Tuple’s limitations
So far we’ve looked at what makes the ValueTuple
special, but in order to know the full story we should look at what limitations exist so we can make an educated descision on when and where to use them.
Let’s take the same person tuple and serialise it to a JSON object. With our named elements we should expect to see an object that resembles our tuple.
var person = (Name: "John", Last: "Smith");
var result = JsonConvert.SerializeObject(person);
Console.WriteLine(result);
// {"Item1":"John","Item2":"Smith"}
Wait, what? where have our keys gone?
To understand what’s going on here we need to take a look at how ValueTuples work.
How the C# 7 ValueTuple type works
Let’s take our GetPerson
method example that returns a named tuple and check out the de-compiled source. No need to install a de-compiler for this, a really handy website called tryroslyn.azurewebsites.net will do everything we need.
// Our code
using System;
public class C {
public void M() {
var person = GetPerson();
Console.WriteLine(person.Name + " is " + person.Age);
}
(string Name, int Age) GetPerson()
{
var name = "John Smith";
var age = 32;
return (name, age);
}
}
You’ll see that when de-compiled, the GetPerson
method is simply syntactic sugar for the following:
// Our code de-compiled
public class C
{
public void M()
{
ValueTuple<string, int> person = this.GetPerson();
Console.WriteLine(person.Item1 + " is " + person.Item2);
}
[return: TupleElementNames(new string[] {
"Name",
"Age"
})]
private ValueTuple<string, int> GetPerson()
{
string item = "John Smith";
int item2 = 32;
return new ValueTuple<string, int>(item, item2);
}
}
If you take a moment to look over the de-compiled source you’ll see two areas of particular interest to us:
First of all, our Console.WriteLine()
method call to our named elements have gone and been replaced with Item1
and Item2
. What’s happened to our named elements? Looking further down the code you’ll see they’ve actually been pulled out and added via the TupleElementNames
attribute.
...
[return: TupleElementNames(new string[] {
"Name",
"Age"
})]
...
This is because the ValueTuple
type’s named elements are erased at runtime, meaning there’s no runtime representation of them. In fact, if we were to view the IL (within the TryRoslyn website switch the Decompiled dropdown to IL), you’ll see any mention of our named elements have completely vanished!
IL_0000: nop // Do nothing (No operation)
IL_0001: ldarg.0 // Load argument 0 onto the stack
IL_0002: call instance valuetype [System.ValueTuple]System.ValueTuple`2<string, int32> C::GetPerson() // Call method indicated on the stack with arguments
IL_0007: stloc.0 // Pop a value from stack into local variable 0
IL_0008: ldloc.0 // Load local variable 0 onto stack
IL_0009: ldfld !0 valuetype [System.ValueTuple]System.ValueTuple`2<string, int32>::Item1 // Push the value of field of object (or value type) obj, onto the stack
IL_000e: ldstr " is " // Push a string object for the literal string
IL_0013: ldloc.0 // Load local variable 0 onto stack
IL_0014: ldfld !1 valuetype [System.ValueTuple]System.ValueTuple`2<string, int32>::Item2 // Push the value of field of object (or value type) obj, onto the stack
IL_0019: box [mscorlib]System.Int32 // Convert a boxable value to its boxed form
IL_001e: call string [mscorlib]System.String::Concat(object, object, object) // Call method indicated on the stack with arguments
IL_0023: call void [mscorlib]System.Console::WriteLine(string) // Call method indicated on the stack with arguments
IL_0028: nop // Do nothing (No operation)
IL_0029: ret // Return from method, possibly with a value
So what does that mean to us as developers?
No reflection on named elements
The absence of named elements in the compiled source means that it’s not possible to use reflection to get those name elements via reflection, which limits ValueTuple
’s utility.
This is because under the bonnet the compiler is erasing the named elements and reverting to the Item1
and Item2
properties, meaning our serialiser doesn’t have access to the properties.
I would highly recommend reading Marc Gravell’s Exploring tuples as a library author post where he discusses a similar hurdle when trying to use tuples within Dapper.
No dynamic access to named elements
This also means when casting your tuple to a dynamic object results in the loss of the named elements. This can be witnessed by running the following example:
var person = (Name: "John", Last: "Smith");
var dynamicPerson = (dynamic)person;
Console.WriteLine(dynamicPerson.Name);
Results in the following error RuntimeBinder exception:
Unhandled Exception: Microsoft.CSharp.RuntimeBinder.RuntimeBinderException: 'System.ValueTuple<string,string>' does not contain a definition for 'Name'
at CallSite.Target(Closure , CallSite , Object )
at CallSite.Target(Closure , CallSite , Object )
at TupleDemo.Program.Main(String[] args) in /Users/josephwoodward/Dev/TupleDemo/Program.cs:line 16
Thanks to Daniel Crabtree’s post for highlighting this!
No using named Tuples in Razor views either (unless they’re declared in your view)
Naturally the name erasure in C# 7 tuples also means that you cannot use the names in your view from your view models. For instance:
public class ExampleViewModel {
public (string Name, int Age) Person => ("John Smith", 30);
}
public class HomeController : Controller
{
...
public IActionResult About()
{
var model = new ExampleViewModel();
return View(model);
}
}
// About.cshtml
@model TupleDemo3.Models.ExampleViewModel
<h1>Hello @Model.Person.Name</h1>
Results in the following error:
'ValueTuple<string, int>' does not contain a definition for 'Name' and no extension method 'Name' accepting a first argument of type 'ValueTuple<string, int>' could be found (are you missing a using directive or an assembly reference?)
Though switching the print statement to @Model.Person.Item1
outputs the result you’d expect.
Conclusion
That’s enough about Tuples for now. Some of the examples used in this post aren’t approaches you’d use in real life, but hopefully go to demonstrate some of the limitations of the new type and where you can and can’t use C# 7’s new ValueTuple type.
Enjoy this post? Don't be a stranger!
Follow me on Twitter at @_josephwoodward and say Hi! I love to learn in the open, meet others in the community and talk Go, software engineering and distributed systems related topics.