System.Tuple
types were introduced in .NET 4.0 with two significant drawbacks: (1) tuple types are classes and (2) there was no language support for constructing/deconstructing them. To solve these issues, C# 7 introduces new language feature as well as a new family of types (*).
Today, if you need to glue together two values to return them from a function or put two values in a hash set you can use System.ValueTuple
types and construct them using a handy syntax:
// Constructing the tuple instance var tpl = (1, 2); // Using tuples with a dictionary var d = new Dictionary<(int x, int y), (byte a, short b)>(); // Tuples with different names are compatible d.Add(tpl, (a: 3, b: 4)); // Tuples have value semantic if (d.TryGetValue((1, 2), out var r)) { // Deconstructing the tuple ignoring the first element var (_, b) = r; // Using named syntax as well as predefined name Console.WriteLine($"a: {r.a}, b: {r.Item2}"); }
(*) System.ValueTuple
types are introduced in .NET Framework 4.7. But you still can use the feature and target lower framework versions, in this case, you have to reference a special nuget package: System.ValueTuple.
- Tuple declaration syntax is similar to function parameter declaration:
(Type1 name1, Type2 name2)
. - Tuple construction syntax is similar to argument construction:
(value1, optionalName: value2)
. - Two tuples with the same element types but with different names are compatible (**):
(int a, int b) = (1, 2)
. - Tuples have value semantic:
(1,2).Equals((a: 1, b: 2))
and(1,2).GetHashCode() == (1,2).GetHashCode()
are bothtrue
. - Tuples do not support
==
and!=
. There is a pending discussion about it on github: “Support for == and != on tuple types”. - Tuples can be “deconstructed” but only into “variable declaration” but not into “out var” or in the
case
block:var (x, y) = (1,2)
– OK,(var x, int y) = (1,2)
– OK,dictionary.TryGetValue(key, out var (x, y))
– not OK,case var (x, y): break;
– not OK. - Tuples are mutable:
(int a, int b) x (1,2); x.a++;
. - Tuple elements can be accessed by the name (if provided) or via generic names like
Item1
,Item2
etc.
(**) We’ll see when this is not the case in a moment.
Tuple element names
Lack of user-defined names makes System.Tuple
types not very useful. I can use System.Tuple
as an implementation detail of a small method but if I need to pass it around I prefer a named type with descriptive property names. New tuple feature addresses this issue quite elegantly: you can specify names for tuple elements and unlike anonymous classed these names are available even across different assemblies.
The C# compiler emits a special attribute TupleElementNamesAttribute
(***) for each tuple type used in a method signature:
(***) The attribute TupleElementNamesAttribute
is special and can’t be used directly in the user code. The compiler emits an error if you try to use it.
public (int a, int b) Foo1((int c, int d) a) => a; [return: TupleElementNames(new[] { "a", "b" })] public ValueTuple<int, int> Foo( [TupleElementNames(new[] { "c", "d" })] ValueTuple<int, int> a) { return a; }
This helps an IDE and the compiler to “see” what the element names are and warn if they used incorrectly:
// Ok: tuple literal can skip element names (int x, int y) tpl = (1, 2); // Warning: The tuple element 'a' is ignored because a different name // or no name is specified by the target type '(int x, int y)'. tpl = (a:1, b:2); // Ok: tuple deconstruction ignore element names var (a, b) = tpl; // x: 2, y: 1. Tuple names are ignored var (y, x) = tpl;
The compiler has stronger requirements for inherited members:
public abstract class Base { public abstract (int a, int b) Foo(); public abstract (int, int) Bar(); } public class Derived : Base { // Error: Cannot change tuple element names when overriding method public override (int c, int d) Foo() => (1, 2); // Error: Cannot change tuple element names when overriding method public override (int a, int b) Bar() => (1, 2); }
Regular method arguments can be freely changed in overriden members, tuple element names in overriden members should exactly match ones from a base type.
Element name inference
C# 7.1 introduces one additional enhancement: element name inference similar to what C# does for anonymous types.
public void NameInference(int x, int y) { // (int x, int y) var tpl = (x, y); var a = new {X = x, Y = y}; // (int X, int y) var tpl2 = (a.X, a.Y); }
Value semantic and mutability
Tuples are mutable value types with elements as public fields. This sounds concerning because we know that mutable value types considered harmful. Here is a small example of their evil nature:
var x = new { Items = new List<int> { 1, 2, 3 }.GetEnumerator() }; while (x.Items.MoveNext()) { Console.WriteLine(x.Items.Current); }
If you’ll run this code you’ll get … an infinite loop. List<T>.Enumerator
is a mutable value type but Items
is a property. This means that x.Items
returns a copy of the original iterator on each loop iteration causing an infinite loop.
But mutable value types are dangerous only when the data is mixed with a behavior: an enumerator holds a state (current element) and has a behavior (an ability to advance an iterator by calling MoveNext
method). This combination can cause issues because it’s so easy to call a method on a copy instead of on an original instance — causing effectively no-op. Here is a set of examples that can cause an unobvious behavior due to a hidden copy of a value type: gist.
But one issue with mutability still remains:
var tpl = (x: 1, y: 2); var hs = new HashSet<(int x, int y)>(); hs.Add(tpl); tpl.x++; Console.WriteLine(hs.Contains(tpl)); // false
Tuples are very useful as keys in dictionaries and can be stored in hash sets due to a proper value semantics. But you should not mutate the state of a tuple variable between different operations with the collection.
Deconstruction
Even though the tuple construction is special to the tuples, deconstruction is generic and can be used with any type.
public static class VersionDeconstrucion { public static void Deconstruct(this Version v, out int major, out int minor, out int build, out int revision) { major = v.Major; minor = v.Minor; build = v.Build; revision = v.Revision; } } var version = Version.Parse("1.2.3.4"); var (major, minor, build, _) = version; // Prints: 1.2.3 Console.WriteLine($"{major}.{minor}.{build}");
Deconstruction uses “duck-typing” approach: if the compiler can find a method called Deconstruct
for a given type – instance method or an extension method – the type is deconstructable.
Aliasing tuples
Once you start using the tuples, you’ll quickly realize that you want to “reuse” a tuple type with named elements in multiple places in your source code. But there are few issues with that. First, C# does not support global aliases for a given type. You can use ‘using’ alias directive, but it creates an alias visible in one file. And second, you can’t even alias the tuple:
// You can't do this: compilation error using Point = (int x, int y); // But you *can* do this using SetOfPoints = System.Collections.Generic.HashSet<(int x, int y)>;
There is a pending discussion on github at “Tuple types in using directives”. So if you’ll find yourself using one tuple type in multiple places you have two options: keep copy-pasting or create a named type.
What casing for elements should I use?
Here is an interesting question: what casing rule we should follow for tuple elements? Pascal case like ElementName
or camel case like elementName
? On one hand, tuple elements should follow the naming rule for public members (i.e. PascalCase), but on the other hand, tuples are just bags with variables and variables are camel cased.
You may consider using a different naming scheme based on the usage and use PascalCase
if a tuple is used as an argument or return type of a method and use camelCase
if a tuple is created locally in the function. But I prefer to use camelCase
all the time.
Conclusion
I’ve found tuples very useful in my day to day job. I need more than one return value from a function, or I need to put a pair of values into a hashset, or I need to change a dictionary and keep not the one value but two, or the key becomes more complicated and I need to extend it with another “field”.
I even use them to avoid closure allocation with methods such a ConcurrentDictionary.TryGetOrAdd
that now takes an extra argument. And in many cases the state is a tuple as well.
The feature is very useful but I really want to see a few enhancements:
- Global aliases: an ability to “name” a tuple and use them in the whole assembly (****).
- Deconstruct a tuple in the pattern matching: in
out var
and incase var
. - Use operator
==
for equality comparison.
(****) I know that this feature is debatable, but I think it’ll be very useful. We can wait for record types, but I’m not sure if the records will be value types or reference types.
0 comments