What are ValueTuples and why not Tuple ?
ValueTuple is a structure that reflects a tuple, just like the original System.Tuple class.
The main difference between Tuple and ValueTuple :
System.ValueTuple is the value type (structure), and System.Tuple is the reference type ( class ). This makes sense when it comes to resource allocation and GC pressure.System.ValueTuple is not only a struct , but also a mutable struct , so be careful when using them as such. Think about what happens when a class contains System.ValueTuple as a field.System.ValueTuple exposes its elements through fields instead of properties.
Prior to C # 7, using tuples was not very convenient. Their field names are Item1 , Item2 , etc., And the language did not provide them with syntactic sugar, as most other languages do (Python, Scala).
When the .NET team decided to include tuples and add syntactic sugar to them at the language level, performance was an important factor. Since ValueTuple is a type of value, you can avoid the GC pressure when using them, because (as implementation details) they will be placed on the stack.
In addition, the struct gets automatic (surface) semantics of equality at runtime, but class does not. Although the development team made sure that the tuples had even more optimized equality, therefore, they realized their own equality for it.
Here is a paragraph from Tuples design Tuples :
Structure or class:
As already mentioned, I suggest doing structs of tuple types, not classes , so that they do not have a distribution penalty associated with them. They should be as light as possible.
Perhaps structs may turn out to be more expensive because assignment copies more value. So if they are assigned much more than they are created, then structs will be a bad choice.
However, by its very motivation, the tuples are ephemeral. You will use them when parts are more important than the whole. Thus, the general scheme will be the creation, return, and immediate deconstruction. In this situation, structures are clearly preferable.
Structures also have a number of other advantages that will become apparent in the following.
Examples:
You can easily see that working with System.Tuple very quickly becomes ambiguous. For example, let's say we have a method that calculates the sum and quantity of a List<Int> :
public Tuple<int, int> DoStuff(IEnumerable<int> values) { var sum = 0; var count = 0; foreach (var value in values) { sum += value; count++; } return new Tuple(sum, count); }
At the receiving end we get:
Tuple<int, int> result = DoStuff(Enumerable.Range(0, 10)); // What is Item1 and what is Item2? // Which one is the sum and which is the count? Console.WriteLine(result.Item1); Console.WriteLine(result.Item2);
The way you can deconstruct tuples of values into named arguments is the real strength of the function:
public (int sum, int count) DoStuff(IEnumerable<int> values) { var res = (sum: 0, count: 0); foreach (var value in values) { res.sum += value; res.count++; } return res; }
And at the receiving end:
var result = DoStuff(Enumerable.Range(0, 10)); Console.WriteLine($"Sum: {result.Sum}, Count: {result.Count}");
Or:
var (sum, count) = DoStuff(Enumerable.Range(0, 10)); Console.WriteLine($"Sum: {sum}, Count: {count}");
The usefulness of the compiler:
If we look under the cover of our previous example, we will see exactly how the compiler interprets ValueTuple when we ask to deconstruct it:
[return: TupleElementNames(new string[] { "sum", "count" })] public ValueTuple<int, int> DoStuff(IEnumerable<int> values) { ValueTuple<int, int> result; result..ctor(0, 0); foreach (int current in values) { result.Item1 += current; result.Item2++; } return result; } public void Foo() { ValueTuple<int, int> expr_0E = this.DoStuff(Enumerable.Range(0, 10)); int item = expr_0E.Item1; int arg_1A_0 = expr_0E.Item2; }
Internally, the compiled code uses Item1 and Item2 , but all of this is abstracted from us, since we are working with a decomposed tuple. A tuple with named arguments is annotated by TupleElementNamesAttribute . If instead of decomposing we use one fresh variable, we get:
public void Foo() { ValueTuple<int, int> valueTuple = this.DoStuff(Enumerable.Range(0, 10)); Console.WriteLine(string.Format("Sum: {0}, Count: {1})", valueTuple.Item1, valueTuple.Item2)); }
Please note that when debugging our application, the compiler should still do some magic (via the attribute), since it would be strange to see Item1 , Item2 .