In 2017 C# 7.0 introduced ValueTuple. This feature makes it convenient to handle lightweight data structures. Here is an example of relying on the C# ValueTuple parenthesis syntax ()
to handle a simple Person
type.
1 2 |
var person = (Name: "Alice", Age: 30, City: "New York"); Console.WriteLine($"Name:{person.Name} Age:{person.Age} City:{person.City}"); |
ValueTuples are available in all .NET Core versions (.NET 8, .NET 7…), in .NET Standard 2.0, and in .NET 4.7 and onward.
Using ValueTuple in Various Coding Scenarios
Tuple is a concept that comes from functional language. ValueTuples were 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 covers all aspects of ValueTuple that a C# developer needs to know to use them effectively.
The Generic ValueTuple Structures
Concretely ValueTuples are:
- 8 generic structures in the namespace System: ValueTuple<T1>, ValueTuple<T1,T2> … ValueTuple<T1,T2,T3,T4,T5,T6,T7,TRest>
- The C# ValueTuple parenthesis syntax
()
introduced in the preceding sections leads the compiler to rely on these structures.
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 ValueTuple 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<…> structure is mutable: this means that its state can be changed just by assigning a ItemX
field.
Another point to consider is comparison: since ValueTuples 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 ValueTuples
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 |
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 ValueTuples 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 equivalent by the compiler. However, for each variable person1, person2 or person3, 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
You can use the Value Tuples named items syntax in C# for parameters and method results:
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 the method 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 in source code.
However, when ValueTuples with named items are publicly visible, the compiler stores the fields’ names in the assembly’s metadata, allowing client assemblies to harness these names. 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 ValueTuple: (string firstName, string lastName, int birthYear). This method, called from the console project, gathers the person value through a var local variable. In the console project, you can still use the field names firstName, lastName, and birthYear, even if this project doesn’t mention them anywhere
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) which exactly means ValueTuple<bool, int> with no named items.
Furthermore, starting with C# 8.0, you can use pattern matching to test and/or deconstruct one or several items of a ValueTuple in a single expression. In the code sample below the Parse(…) method returns a ValueTuple (bool, int). The expression if(Parse(str) is (true, int result)) tests 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 uses of tuple: a lightweight way to obtain multiple values from a method call. Lightweight here has 2 meanings:
- There is no need to declare a new class or a structure to type the method result. The concise syntax (bool, int) is enough.
- The Garbage Collector doesn’t keep track of the value returned because (bool, int) is a structure.
The Many Facets of ValueTuple
ValueTuple and Deconstruction
The expression if(Parse(str) is (true, int result)) mixes pattern matching and deconstruction, but you can fully deconstruct a ValueTuple 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 item fields to assign the local variables. Thus the IL code looks like this:
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] |
ValueTuple 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 ValueTuple. However one can also test multiple items at once this way:
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 } |
ValueTuple and Hash Tables
Another common usage of ValueTuples 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 ValueTuples are also useful for collections that are not hash tables, like List<(bool,int)>.
ValueTuple and async methods
Async methods cannot have out parameters. Thus ValueTuple 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); } |
ValueTuple and the params keyword
It is worth noting that the keyword params works well with ValueTuples which makes it well suited to write methods that accept multiple key-value pairs like this one:
1 2 3 4 |
Method((true,10), (false,9), (true,1)); void Method(params (bool, int)[] array) { // ... } |
ValueTuple and Serialization
To serialize ValueTuples using System.Text.Json, you must use the serialization option IncludeFields = true, because the items in value tuples are fields, 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 were once serializable through BinaryFormatter, but binary serialization has been discarded in .NET 5.0.
ValueTuple vs. Its Alternatives
ValueTuple 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>
However, these tuple classes have two limitations leading to the introduction of the ValueTuple generic structures later:
- Tuple, as classes, don’t fit well in lightweight scenarios like method return values. The Garbage Collector tracks each instance, which is not performance-wise.
- Additionally, you cannot change the value of items in a Tuple because they are immutable
In most real-world scenarios you will favor ValueTuple 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)); |
ValueTuple 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, the compiler declares anonymous classes as internal. Thus they are not visible outside their projects. This means that in most scenarios ValueTuples represent a better choice 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 ValueTuple cannot.
ValueTuple 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 turn tedious when this syntax applies to many methods (say 5+ methods). In such a situation, it is preferable to write your custom lightweight record struct type such as:
1 |
record struct Result<T>(bool Success, T Value); |
Also, notice that ValueTuples don’t work with type alias. One cannot write:
1 |
using Result = (bool success, int value); |
Advanced ValueTuple
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 this interface in your user code. This is because Length and the indexer are implemented as explicit interfaces in ValueTuple<…> structure. This implies that to use it, one must cast the tuple as an 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. The Garbage Collector’s management of this object degrades performance. 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
TheSystem.ValueTuple structure 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 ValueTuple is more concise. For example, let’s compare the syntaxes 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); |
ValueTuple 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 ValueTuples with more than 8 items. The compiler provides special support for it under the hood. In the code below both ValueTuple 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()); |
Conclusion
ValueTuple in C# stands out as a significant enhancement, streamlining code complexity with its performance-oriented and lightweight architecture. Its design makes it a more advantageous choice over traditional options in numerous situations.
The C# and .NET teams invested a lot of effort in ValueTuples 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 ValueTuples 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