Once again, in this series of posts I look at the parts of the .NET Framework that may seem trivial, but can really help improve your code by making it easier to write and maintain. This week, we look at the System.Tuple class and the handy factory methods for creating a Tuple by inferring the types.
What is a Tuple?
The System.Tuple is a class that tends to inspire a reaction in one of two ways: love or hate. Simply put, a Tuple is a data structure that holds a specific number of items of a specific type in a specific order. That is, a Tuple<int, string, int> is a tuple that contains exactly three items: an int, followed by a string, followed by an int. The sequence is important not only to distinguish between two members of the tuple with the same type, but also for comparisons between tuples.
Some people tend to love tuples because they give you a quick way to combine multiple values into one result. This can be handy for returning more than one value from a method (without using out or ref parameters), or for creating a compound key to a Dictionary, or any other purpose you can think of. They can be especially handy when passing a series of items into a call that only takes one object parameter, such as passing an argument to a thread's startup routine. In these cases, you do not need to define a class, simply create a tuple containing the types you wish to return, and you are ready to go?
On the other hand, there are some people who see tuples as a crutch in object-oriented design. They may view the tuple as a very watered down class with very little inherent semantic meaning. As an example, what if you saw this in a piece of code:
1: var x = new Tuple<int, int>(2, 5);
What are the contents of this tuple? If the tuple isn't named appropriately, and if the contents of each member are not self evident from the type this can be a confusing question. The people who tend to be against tuples would rather you explicitly code a class to contain the values, such as:
1: public sealed class RetrySettings
2: {
3: public int TimeoutSeconds { get; set; }
4: public int MaxRetries { get; set; }
5: }
Here, the meaning of each int in the class is much more clear, but it's a bit more work to create the class and can clutter a solution with extra classes.
So, what's the correct way to go? That's a tough call. You will have people who will argue quite well for one or the other. For me, I consider the Tuple to be a tool to make it easy to collect values together easily. There are times when I just need to combine items for a key or a result, in which case the tuple is short lived and so the meaning isn't easily lost and I feel this is a good compromise. If the scope of the collection of items, though, is more application-wide I tend to favor creating a full class.
Finally, it should be noted that tuples are immutable. That means they are assigned a value at construction, and that value cannot be changed. Now, of course if the tuple contains an item of a reference type, this means that the reference is immutable and not the item referred to.
Tuples from 1 to N
Tuples come in all sizes, you can have as few as one element in your tuple, or as many as you like. However, since C# generics can't have an infinite generic type parameter list, any items after 7 have to be collapsed into another tuple, as we'll show shortly.
So when you declare your tuple from sizes 1 (a 1-tuple or singleton) to 7 (a 7-tuple or septuple), simply include the appropriate number of type arguments:
1: // a singleton tuple of integer
2: Tuple<int> x;
3:
4: // or more
5: Tuple<int, double> y;
6:
7: // up to seven
8: Tuple<int, double, char, double, int, string, uint> z;
Anything eight and above, and we have to nest tuples inside of tuples. The last element of the 8-tuple is the generic type parameter Rest, this is special in that the Tuple checks to make sure at runtime that the type is a Tuple. This means that a simple 8-tuple must nest a singleton tuple (one of the good uses for a singleton tuple, by the way) for the Rest property.
1: // an 8-tuple
2: Tuple<int, int, int, int, int, double, char, Tuple<string>> t8;
3:
4: // an 9-tuple
5: Tuple<int, int, int, int, double, int, char, Tuple<string, DateTime>> t9;
6:
7: // a 16-tuple
8: Tuple<int, int, int, int, int, int, int, Tuple<int, int, int, int, int, int, int, Tuple<int,int>>> t14;
Notice that on the 14-tuple we had to have a nested tuple in the nested tuple. Since the tuple can only support up to seven items, and then a rest element, that means that if the nested tuple needs more than seven items you must nest in it as well.
Constructing tuples
Constructing tuples is just as straightforward as declaring them. That said, you have two distinct ways to do it. The first is to construct the tuple explicitly yourself:
1: var t3 = new Tuple<int, string, double>(1, "Hello", 3.1415927);
This creates a triple that has an int, string, and double and assigns the values 1, "Hello", and 3.1415927 respectively. Make sure the order of the arguments supplied matches the order of the types! Also notice that we can't half-assign a tuple or create a default tuple. Tuples are immutable (you can't change the values once constructed), so thus you must provide all values at construction time.
Another way to easily create tuples is to do it implicitly using the System.Tuple static class's Create() factory methods. These methods (much like C++'s std::make_pair method) will infer the types from the method call so you don't have to type them in. This can dramatically reduce the amount of typing required especially for complex tuples!
1: // this 4-tuple is typed Tuple<int, double, string, char>
2: var t4 = Tuple.Create(42, 3.1415927, "Love", 'X');
Notice how much easier it is to use the factory methods and infer the types? This can cut down on typing quite a bit when constructing tuples. The Create() factory method can construct from a 1-tuple (singleton) to an 8-tuple (octuple), which of course will be a octuple where the last item is a singleton as we described before in nested tuples.
Accessing tuple members
Accessing a tuple's members is simplicity itself… mostly. The properties for accessing up to the first seven items are Item1, Item2, …, Item7. If you have an octuple or beyond, the final property is Rest which will give you the nested tuple which you can then access in a similar matter. Once again, keep in mind that these are read-only properties and cannot be changed.
1: // for septuples and below, use the Item properties
2: var t1 = Tuple.Create(42, 3.14);
3:
4: Console.WriteLine("First item is {0} and second is {1}",
5: t1.Item1, t1.Item2);
6:
7: // for octuples and above, use Rest to retrieve nested tuple
8: var t9 = new Tuple<int, int, int, int, int, int, int,
9: Tuple<int, int>>(1,2,3,4,5,6,7,Tuple.Create(8,9));
10:
11: Console.WriteLine("The 8th item is {0}", t9.Rest.Item1);
Tuples are IStructuralComparable and IStructuralEquatable
Most of you know about IComparable and IEquatable, what you may not know is that there are two sister interfaces to these that were added in .NET 4.0 to help support tuples. These IStructuralComparable and IStructuralEquatable make it easy to compare two tuples for equality and ordering. This is invaluable for sorting, and makes it easy to use tuples as a compound-key to a dictionary (one of my favorite uses)!
Why is this so important? Remember when we said that some folks think tuples are too generic and you should define a custom class? This is all well and good, but if you want to design a custom class that can automatically order itself based on its members and build a hash code for itself based on its members, it is no longer a trivial task! Thankfully the tuple does this all for you through the explicit implementations of these interfaces.
For equality, two tuples are equal if all elements are equal between the two tuples, that is if t1.Item1 == t2.Item1 and t1.Item2 == t2.Item2, and so on. For ordering, it's a little more complex in that it compares the two tuples one at a time starting at Item1, and sees which one has a smaller Item1. If one has a smaller Item1, it is the smaller tuple. However if both Item1 are the same, it compares Item2 and so on.
For example:
1: var t1 = Tuple.Create(1, 3.14, "Hi");
2: var t2 = Tuple.Create(1, 3.14, "Hi");
3: var t3 = Tuple.Create(2, 2.72, "Bye");
4:
5: // true, t1 == t2 because all items are ==
6: Console.WriteLine("t1 == t2 : " + t1.Equals(t2));
7:
8: // false, t1 != t2 because at least one item different
9: Console.WriteLine("t2 == t2 : " + t2.Equals(t3));
The actual implementation of IComparable, IEquatable, IStructuralComparable, and IStructuralEquatable is explicit, so if you want to invoke the methods defined there you'll have to manually cast to the appropriate interface:
1: // true because t1.Item1 < t3.Item1, if had been same would check Item2 and so on
2: Console.WriteLine("t1 < t3 : " + (((IComparable)t1).CompareTo(t3) < 0));
So, as I mentioned, the fact that tuples are automatically equatable and comparable (provided the types you use define equality and comparability as needed) means that we can use tuples for compound keys in hashing and ordering containers like Dictionary and SortedList:
1: var tupleDict = new Dictionary<Tuple<int, double, string>, string>();
2:
3: tupleDict.Add(t1, "First tuple");
4: tupleDict.Add(t2, "Second tuple");
5: tupleDict.Add(t3, "Third tuple");
Because IEquatable defines GetHashCode(), and Tuple's IStructuralEquatable implementation creates this hash code by combining the hash codes of the members, this makes using the tuple as a complex key quite easy! For example, let's say you are creating account charts for a financial application, and you want to cache those charts in a Dictionary based on the account number and the number of days of chart data (for example, a 1 day chart, 1 week chart, etc):
1: // the account number (string) and number of days (int) are key to get cached chart
2: var chartCache = new Dictionary<Tuple<string, int>, IChart>();
Summary
The System.Tuple, like any tool, is best used where it will achieve a greater benefit. I wouldn't advise overusing them, on objects with a large scope or it can become difficult to maintain. However, when used properly in a well defined scope they can make your code cleaner and easier to maintain by removing the need for extraneous POCOs and custom property hashing and ordering.
They are especially useful in defining compound keys to IDictionary implementations and for returning multiple values from methods, or passing multiple values to a single object parameter.
Tweet
Technorati Tags: C#,.NET,Tuple,Little Wonders