C# 7.0 introduced Value Tuples which represent both a set of structures in the .NET Base Class Library (BCL) and some convenient C# syntax. Value tuples are available in all .NET Core versions, in .NET Standard 2.0 in and in .NET 4.7 and onward.
Tuple is a concept that comes from functional language and it was introduced in C# to simplify code in a wide range of scenarios including:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
// Method returning multiple items (bool, int) Parse(string str) { ... ... if (Parse(str) is (true, int result)) { // Create a temporary meaningful property-bag type (string firstName, string lastName, int birthYear) person2 = ("Léna", "Smacchia", 2014); // Pattern matching switch (Parse(str)) { case (true, 3): case (false, 8): // Deconstruction var (success, result) = Parse(str); // Collections var list = new List<(bool, int)> { (true, 10), (false, 8), }; // HashSet and Dictionary var dico = new Dictionary<(bool, int)> { (true, 10), "true and 10" }, (false, 2), "false and 2" }, }; // Async Methods static async Task<(bool, int)> MethodAsync() { ... // params Methods Method((true, 10), (false, 9), (true, 1)); void Method(params (bool, int)[] array) { ... |
The present article is covering all aspects of value tuples that a C# developer needs to know to use them effectively.
The generic ValueTuple structures
There are 8 generic structures in the namespaces System
that are: ValueTuple<T1>, ValueTuple<T1,T2> (…) ValueTuple<T1,T2,T3,T4,T5,T6,T7,TRest>
The choice of structure over class has been driven by performance. Instances of structures are lightweight: they are values stored on the thread stack or within an object’s binary layout. Thus these values are not tracked by the Garbage Collector at runtime, hence the substantial performance gain.
These structures have a field named ItemX
for each generic type TX
:
1 2 3 4 |
public struct ValueTuple<T1,T2> { public T1 Item1; public T2 Item2; } |
Normally a field with a public
visibility is an anti-pattern and a property is preferred for encapsulation purposes. But value tuple is all about performance and since properties involve extra getter and setter method calls, this public-field design has been favored. A consequence is that an instance of a ValueTuple<…> structures is mutable: this means that its state can be changed just by assigning an Item
field.
Another point to consider is comparison: since value tuples are value types we get value equality: two tuples are considered equals if all their fields are considered equals one by one:
1 2 3 4 5 |
var tupleA = new ValueTuple<bool, int>(true, 9); var tupleB = new ValueTuple<bool, int>(true, 9); Assert.IsTrue(tupleA == tupleB); tupleB.Item2 = 8; Assert.IsFalse(tupleA == tupleB); |
The named items C# syntax over value tuples
C# lets the developer rename each ItemX
field to make the code more readable. Here is a compilable code sample:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
// Creating a ValueTuple through generic instantiation var person1 = new ValueTuple<string, string, int>("Patrick", "Smacchia", 1975); Assert.IsTrue(person1.Item1 == "Patrick"); Assert.IsTrue(person1.Item2 == "Smacchia"); Assert.IsTrue(person1.Item3 == 1975); // Just a quick check that ValueTuple is mutable! person1.Item1 = "Paul"; Assert.IsTrue(person1.Item1 == "Paul"); // Creating a ValueTuple with the C# named items syntax (string firstName, string lastName, int birthYear) person2 = ("Léna", "Smacchia", 2014); // Accessing items via the C# named items syntax Assert.IsTrue(person2.firstName == "Léna"); Assert.IsTrue(person2.lastName == "Smacchia"); Assert.IsTrue(person2.birthYear == 2014); // Accessing items via the ItemX fields is still possible, but named items make the code clearer Assert.IsTrue(person2.Item1 == "Léna"); Assert.IsTrue(person2.Item2 == "Smacchia"); Assert.IsTrue(person2.Item3 == 2014); // person1 can be assigned to person2 and vice-versa, the C# compiler considers these types as equivalent person2 = person1; Assert.IsTrue(person2.firstName == "Paul"); // person2 can be assigned to person3. // Even though the items' names are different, the C# compiler considers these types as equivalent (string fn, string ln, int by) person3 = ("Léna", "Smacchia", 2014); person3 = person2; Assert.IsTrue(person3.fn == "Paul"); |
Notice how the 3x types…
- ValueTuple<string,string,int>
- (string firstName, string lastName, int birthYear)
- (string fn, string ln, int by)
…are considered as equivalent by the compiler. However, for each reference the C# compiler keeps track of the items names. For example person3 is typed as (string fn, string ln, int by). Replacing person3.fn with person3.firstName provokes a compiler error.
Named item C# syntax for method parameters and result
The syntax with named item can be used for parameter and method result:
1 2 3 4 5 6 7 8 9 10 11 |
(string firstName, string lastName, int birthYear) person1 = ("Léna", "Smacchia", 2014); var person2 = IncreaseBirthYear(person1); Assert.IsTrue(person2.birthYear == 2015); // Notice that person1.birthYear is left untouched since ValueTuple is a structure and not a class Assert.IsTrue(person1.birthYear == 2014); (string firstName, string lastName, int birthYear) IncreaseBirthYear((string firstName, string lastName, int birthYear) person) { person.birthYear++; return person; } |
In the code sample above, notice that person1.birthYear is left untouched by the call to IncreaseBirthYear(). This is because ValueTuple<..> is a structure and not a class so it is passed by-value to IncreaseBirthYear(). You can still use the ref keyword to pass that value person1 by-reference through a managed pointer and be able to reflect state changes performed in the method IncreaseBirthYear().
1 2 3 4 5 6 7 8 9 10 11 |
(string firstName, string lastName, int birthYear) person1 = ("Léna", "Smacchia", 2014); var person2 = IncreaseBirthYear(ref person1); Assert.IsTrue(person2.birthYear == 2015); // Now the change is reflected thanks to by-reference parameter passing Assert.IsTrue(person1.birthYear == 2015); (string firstName, string lastName, int birthYear) IncreaseBirthYear(ref (string firstName, string lastName, int birthYear) person) { person.birthYear++; return person; } |
Decompiling the C# named items syntax
Interestingly enough, if we decompile the first named item syntax code sample, we can see that the compiled IL code doesn’t keep a trace of ItemX fields name.
However when value tuples with named items are publicly visible, the fields’ names get stored in the assembly’s metadata so the names can be harnessed from client assemblies. The screenshot below shows a solution with a console project referencing a library project. The library project has a method GetMethod() that returns this named value tuple: (string firstName, string lastName, int birthYear). This method is called from the console project and the person value is gathered through a var local variable. We can see that the field’s names firstName, lastName and birthYear are still usable from the console project, despite being mentioned nowhere in this project.
The C# value tuple parentheses syntax
The named items syntax can be simplified with just a parentheses syntax. For example (bool success,int result) can be simplified as (bool,int) that exactly means ValueTuple<bool,int> with no named items.
Moreover since C# 8.0 pattern matching can be used to test and / or deconstruct one or several value tuples items in a single expression. In the code sample below the Parse(…) method returns a value tuple (bool,int). The expression if(Parse(str) is (true, int result)) test the boolean value and deconstructs the integer item into a local variable
1 2 3 4 5 6 7 8 9 10 11 12 |
string str = "691"; if (Parse(str) is (true, int result)) { Console.WriteLine($"{result} parsed"); } else { Console.WriteLine($"{str} is not an integer"); } (bool, int) Parse(string str) { return int.TryParse(str, out int i) ? (true, i) : (false, -1); } |
The code sample above shows one of the most common usage of tuple: a lightweight way to obtain multiple values from a method call. Lightweight here has 2 meaning:
- There is no need to declare a new class or a structure to type the method result, (bool,int) is enough.
- The Garbage Collector doesn’t keep track of the value returned because (bool,int) is a structure.
Value Tuple and Deconstruction
The expression if(Parse(str) is (true, int result)) shows a mix of pattern matching and deconstruction but a value tuple can be fully deconstructed this way:
1 2 |
string str = "691"; var (success,result) = Parse(str); |
Thanks to deconstruction we obtain two local variables from a single expression. However a structure ValueTuple<…> doesn’t have a Deconstruct(…) method. Instead the C# compiler has a special behavior: it reads the items fields to assign the local variables. Thus the IL code looks like:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
[0] string str, [1] bool success, [2] int32 result, IL_0000: ldstr "691" IL_0005: stloc.0 IL_0006: ldloc.0 IL_0007: call valuetype [System.Runtime]System.ValueTuple`2<bool, int32> Program::'<<Main>$>g__Parse|0_0'(string) IL_000c: dup IL_000d: ldfld !0 valuetype [System.Runtime]System.ValueTuple`2<bool, int32>::Item1 IL_0012: stloc.1 // Store the Item1 value within success local variable [1] IL_0013: ldfld !1 valuetype [System.Runtime]System.ValueTuple`2<bool, int32>::Item2 IL_0012: stloc.2 // Store the Item2 value within success local variable [2] |
Value Tuple and Pattern Matching
We saw that the expression if(Parse(str) is (true, int result)) uses pattern matching to test a single item of a value tuple. However multiple items can be tested at once:
1 2 3 4 5 6 7 |
string str = "691"; switch(Parse(str)) { case (true, 3): // Do something case (false, 8): // Do something case (_, 6): // Do something default: // Do something } |
Value Tuple and hash table
Another common usage of value tuples is in hash tables like HashSet<T> or Dictionary<TKey,TValue> when the key or the value might be represented by multiple items. Here is an example:
1 2 3 4 5 6 7 |
var dico = new Dictionary<(bool, int), string> { { (true, 10), "true and 10" }, { (false, 2), "false and 2" }, }; Assert.IsTrue(dico.TryGetValue((true, 10), out string str)); Assert.IsTrue(str == "true and 10"); Assert.IsFalse(dico.TryGetValue((true, 2), out str)); |
Of course value tuple are also useful for collections that are not hash tables, like List<(bool,int)>.
Value Tuple and async method
Async methods cannot have out parameters. Thus value tuple is an ideal way to return multiple items from an asynchronous call:
1 2 3 4 5 6 7 8 9 10 |
Task<(bool, int)> task = MethodAsync(); await task; Assert.IsTrue(task.Result == (true, 123)); static async Task<(bool, int)> MethodAsync() { for (int i = 0; i < 5; i++) { await Task.Delay(100); } return (true, 123); } |
Value Tuple and the params keyword
It is worth to note that the keyword params works well with value tuples which makes it well suited to write methods that accepts multiple key value pairs like this one:
1 2 3 4 |
Method((true,10), (false,9), (true,1)); void Method(params (bool, int)[] array) { // ... } |
The ITuple interface
Each ValueTuple<…> structure implements the interface System.Runtime.CompilerServices.ITuple defined as:
1 2 3 4 |
public interface ITuple { int Length { get; } object? this[int index] { get; } } |
Being declared in the namespace System.Runtime.CompilerServices is an indication you shouldn’t use it in your user code. This is because Length and the indexer are implemented as explicit interface in ValueTuple<…> structure. This means that to be used, the tuple needs to be casted as a ITuple reference.
1 2 3 4 |
ITuple tuple = (true, 10); Assert.IsTrue(tuple.Length == 2); Assert.IsTrue((bool)tuple[0]); Assert.IsTrue((int)tuple[1] == 10); |
Casting a structure value to a reference means boxing. Boxing is when the runtime creates an object to host a structure value. This object gets tracked by the Garbage Collector and thus, performance gets hurt. By decompiling the sample code above we can see the box IL instruction.
1 2 3 4 |
IL_0000: ldc.i4.1 IL_0001: ldc.i4.s 10 IL_0003: newobj instance void valuetype [System.Runtime]System.ValueTuple`2<bool, int32>::.ctor(!0, !1) IL_0008: box valuetype [System.Runtime]System.ValueTuple`2<bool, int32> |
The ValueTuple structure
The structure System.ValueTuple proposes several static methods like Create(), Equals() or GetHashCode(). Most of the time you won’t use those methods explicitly because the C# syntax for value tuple is more concise, for example to create a tuple:
1 2 3 |
var tuple1 = (true, 10); // More concise syntax to create the same tuple var tuple2 = ValueTuple.Create(true, 10); Assert.IsTrue(tuple1 == tuple2); |
Maybe you’ll want to use the method Create() to obtain a tuple with no item that might be useful in some algorithms that rely on ITuple to process any kind of tuple. Such empty tuple is an instance of the ValueTuple not-generic structure.
1 2 3 4 |
ITuple tuple0 = ValueTuple.Create(); Assert.IsTrue(tuple0.Length == 0); ITuple tuple1 = new ValueTuple(); Assert.IsTrue(tuple1.Length == 0); |
Value Tuple with more than 8 items
We saw that a ValueTuple<…> structure has at most 8 items. However in the structure ValueTuple<T1,T2,T3,T4,T5,T6,T7,TRest> the generic type TRest can be typed as a ValueTuple<…> that contains additional items. C# can accept value tuples with more than 8 items and provide special support for it under the hood. In the code below both value tuple are instances of ValueTuple<int,int,int,int,int,int,int,ValueTuple<int,int,int,int,int>>, but their length is different depending on how they get instantiated:
1 2 3 4 5 6 7 8 9 10 |
ITuple tupleA = (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12); Assert.IsTrue(tupleA.Length == 12); for(var i = 0; i < tupleA.Length; i++) { Assert.IsTrue((int)tupleA[i] == i+1); } ITuple tupleB = (1, 2, 3, 4, 5, 6, 7, (8, 9, 10, 11, 12)); Assert.IsTrue(tupleB.Length == 8); // Those are not equivalent types Assert.IsTrue(tupleA.GetType() != tupleB.GetType()); |
Value Tuple and Serialization
Value tuples can be serialized through System.Text.Json but the serialize option IncludeFields = true must be used since items are fields and not properties:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
var tupleA = (success:true, result:10); var options = new JsonSerializerOptions { IncludeFields = true }; var json = JsonSerializer.Serialize(tupleA, options); // Notice that item names are not used Assert.IsTrue(json == """{"Item1":true,"Item2":10}"""); // raw literal string syntax var tupleB = JsonSerializer.Deserialize<(bool,int)>(json, options); Assert.IsTrue(tupleA == tupleB); // The same way item names are not used in ToString() Assert.IsTrue(tupleA.ToString() == "(True, 10)"); |
Value tuples used to be serializable through BinaryFormatter but binary serialization has been discarded in .NET 5.0.
Value Tuple vs. Tuple
Since .NET 4.0 we had the 8 Tuple classes: Tuple<T1>, Tuple<T1,T2> (…) Tuple<T1,T2,T3,T4,T5,T6,T7,TRest>
The value tuple structures were introduced later because the tuple classes have 2 limitations:
- Tuple don’t fit in lightweight scenarios like method return value. Tuples are classes and thus each instance created gets tracked by the Garbage Collector which is not performance wise.
- Tuple are immutable, the items value cannot be changed.
In most real world scenario you will favor value tuple structures over tuple classes. Let’s notice the ToValueTuple() method presented by each Tuple<…> class:
1 2 |
var tuple = new Tuple<bool,int>(true, 10); Assert.IsTrue(tuple.ToValueTuple() == (true,10)); |
Value Tuple vs. Anonymous Type
C# 3.0 introduced anonymous types to be used in LINQ queries. Here is an example:
1 2 3 4 5 |
var durations = new[] { new TimeSpan(1,0,0), new TimeSpan(2,0,0), new TimeSpan(3,0,0),}; foreach (var anonymous in durations.Select( duration => new { Readable = $"{duration:c}", duration.Ticks })) { Console.WriteLine($"Ticks: {anonymous.Ticks}, Readable: {anonymous.Readable}"); } |
The expression new { Readable = $”{duration:c}”, duration.Ticks } forces the C# compiler to create this anonymous class in the compiled assembly.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
[CompilerGenerated] [DebuggerDisplay("\\{ Readable = {Readable}, Ticks = {Ticks} }", Type = "<Anonymous Type>")] internal sealed class <>f__AnonymousType0<<Readable>j__TPar, <Ticks>j__TPar> { private readonly <Readable>j__TPar <Readable>i__Field; public <Readable>j__TPar Readable => <Readable>i__Field; private readonly <Ticks>j__TPar <Ticks>i__Field; public <Ticks>j__TPar Ticks => <Ticks>i__Field; [DebuggerHidden] public <>f__AnonymousType0(<Readable>j__TPar Readable, <Ticks>j__TPar Ticks) { <Readable>i__Field = Readable; <Ticks>i__Field = Ticks; } ... Equals() GetHashCode() ToString() methods } |
As Tuple<…>, anonymous types are classes and their properties are immutable. Moreover anonymous classes are declared as internal and are not visible outside their projects. This means that in most scenarios value tuples are better suited than anonymous types because they are lightweight.
However notice that anonymous type can be used in expression tree advanced scenarios (when IQueryable<T> is required) and value tuples cannot.
Value Tuple vs. Record Struct
Having to declare the tuple in method return type as in…
- (bool, int) Parse(string str) {…
- or in (bool success, int result) Parse(string str) {…
…can become tedious if many methods are impacted (say 5+ methods). In such situation better write your custom lightweight record struct type such as:
1 |
record struct Result<T>(bool Success, T Value); |
Also notice that value tuples don’t work with type alias, one cannot write:
1 |
using Result = (bool success, int value); |
Conclusion
Value tuple is a bless because it can really simplify most C# code. Thanks to its performance-centric and lightweight design, value tuple is preferable over alternatives in most scenarios. The C# and .NET teams invested a lot of effort in value tuples to make them work seamlessly with other features such as deconstruction, pattern matching, async method, collection, hash table or JSON serialization. If you are not using value tuples yet in your code, we hope that this article convinced you to change your habits and write better C# code with them.
Hey Patrick,
thank you so much for this overview. It’s awesome!
I enjoy every article you post!
Just a small remark:
I think that in the first code block the line 14
“var dico = new List”
should be replaced with
“var dico = new Dictionary”
to match the object initialization.
Cheers
Julian
Fixed!
Thanks Julian for the kind words, I am glad this content can help