The
System.Tuple types were introduced in .NET 4.0 with two major drawbacks:
- Tuple types are classes;
- There is no language support for their creation / deconstruction.
To solve these problems, C # 7 introduces a new language feature as well as a new type family (*).
Today, if you need to glue two values together to return them from a function or place two values in a hash set, you can use the
System.ValueTuple types and create them using a convenient syntax:
(*)
System.ValueTuple types are represented in the .NET Framework 4.7. But you can use them in earlier versions of the framework, in which case you need to add a special package to the project nuget:
System.ValueTuple .
')
- The syntax of the Tuple declaration is similar to the function parameter declaration: (Type1 name1, Type2 name2) .
- The syntax for creating Tuple instances is similar to passing arguments: (value1, optionalName: value2) .
- Two tuples with the same element types, but with different names, are compatible (**): (int a, int b) = (1, 2) .
- Tuples have semantics of values:
(1,2). Equals ((a: 1, b: 2)) and (1,2). GetHashCode () == (1,2). GetHashCode () is true . - Tuples do not support == and ! = . This feature is discussed in github: “Support == and! = For tuple types . ”
- Tuples can be “deconstructed”, but only in “variable declaration”, but not in “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)) is not OK, case var (x, y): break; - not OK. - Tuples are changed: (int a, int b) x (1,2); x.a ++; .
- Elements of a tuple can be obtained by name (if specified in the declaration) or through common names, such as Item1, Item2 , etc.
(**) We will soon see that this is not always the case.
Named elements of a tuple
The lack of user names makes the
System.Tuple types not very useful. I can use
System.Tuple as part of a small method implementation, but if I need to pass an instance of it, I prefer a named type with descriptive property names. The tuples in C # 7 quite elegantly solve this problem: you can specify names for the elements of a tuple and, unlike anonymous classes, these names are available even in different assemblies.
The C # compiler generates a special attribute
TupleElementNamesAttribute (***) for each type of tuple used in the method signature:
(***) The TupleElementNamesAttribute attribute is special and cannot be used directly in the user code. The compiler gives 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 attribute helps the IDE and compiler to "see" the names of the elements and to warn if they are used incorrectly:
The compiler has higher requirements for inherited members:
public abstract class Base { public abstract (int a, int b) Foo(); public abstract (int, int) Bar(); } public class Derived : Base {
The usual method arguments can be freely changed in the overridden members, but the names of the elements of the tuples in the overridden members must exactly match the names from the base type.
Item name display
C # 7.1 has one additional improvement: the output of the name of a tuple element is similar to what C # does for anonymous types.
public void NameInference(int x, int y) {
Semantics of values and variability.
Tuples are mutable significant types. We know that mutable significant types are 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 run this code, you get ... an endless loop. The List .Enumerator list is a mutable type, and the
Items property. This means that
x.Items returns a copy of the source iterator at each iteration of the loop, causing an infinite loop.
But changeable significant types are dangerous only when the data is mixed with the behavior: the Enumerator contains the state (the current element) and has the behavior (the ability to advance the iterator by calling the
MoveNext method). This combination can cause problems, because it is easy to call a method on a copy, instead of the original instance, which leads to the no-op effect (No Operation). Here is a set of examples that can cause unobvious behavior due to a hidden copy of the value type:
gist .
Tuples have a state, but not a behavior, so the above problems do not apply to them. But one problem with volatility 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));
Tuples are very useful as keys in dictionaries and can be used as keys due to value semantics. But you should not change the state of the variable key between different operations with the collection.
Deconstruction
Although C # has a special syntax for instantiating tuples, deconstruction is a more general feature 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;
Parsing (deconstructing) a tuple uses the “duck typing” approach: if the compiler can find the
Deconstruct method for a given type — an instance method or an extension method — the type is parsed.
Tuple aliases
After you start using tuples, you will quickly realize that you want to “reuse” a type of tuple with named elements in several places in the source code. But this is not so simple.
First, C # does not support global aliases for a given type. You can use the alias directive using the alias, but it creates an alias visible in one file.
Secondly, you cannot even use this feature with tuples:
Now on github in the topic
"Types of Tuple using directives" is a discussion of this problem. Therefore, if you find that you are using the same type of tuple in several places, you have two options: either copy to the types throughout the code base or create a named type.
What naming convention for elements should I use?
Pascal case, for example
ElementName , or camel case, for example
elementName ? On the one hand, tuple elements must follow the naming rule for public members (i.e.
PascalCase ), but, on the other hand, tuples are just a repository for variables, and variables are named with
camelase .
You can use the following approach:
- PascalCase if the tuple is used as an argument or return type of the method;
- camelCase if the tuple is created locally in the function.
But I prefer to use camelCase all the time.
Conclusion
I found tuples very useful in daily work. I need more than one return value from a function, or I need to put a couple of values in a hash set, or I need to change the dictionary and save not one value, but two, or the key becomes more complex, and I need to expand it with another field.
I even use them to avoid blocking by using methods such as
ConcurrentDictionary.TryGetOrAdd , which now takes an extra argument. And in many cases, the state is also a tuple.
These features are very useful, but I really want to see a few improvements:
- Global pseudonyms: the ability to "call" a tuple and use them throughout the assembly (****).
- Analysis of the tuple in comparison with the sample: in out var and in case var .
- Use the == operator to compare equality.
(****) I know that this function is controversial, but I think it will be very useful. We can wait for the
Record types, but I'm not sure whether the records will be significant types or reference types.