Many times when you talk with developers about anonymous types or
tuples you will often get a negative gut-reaction. There's probably a
bit of that hard-core object-oriented designer in all of us that cringes
a bit inside at the idea of using one of these "lightweight" types
instead of building a full-fledged class.
But, when used properly, these types can actually improve the
maintainability of the code-base when used judiciously because they
eliminate the need of creating dozens of one-use, simple types that can
simply clutter up a project.
In addition, as we will see, these types have a few tricks up their sleeves that can actually, in some circumstances, make them safer than constructing your own type.
Tuples
The Tuple is a family of generic types that hold specific numbers of items of specific types in a specific order. For example:
var triple = new Tuple<int, string, int>(13, "Hello", 42);
The tuple above contains exactly three items: an int, followed by a string, followed by an int. Also notice that the values must be passed in at construction. This is because the Tuple is immutable, and once the properties are set, they cannot be changed.
To access the members of a Tuple, you use the properties named Item1, Item2, ... Item7 where N is the position of the item in the constructor arguments.
Console.WriteLine("The first item is: " + triple.Item1);
Console.WriteLine("The second item is: " + triple.Item2);
Console.WriteLine("The third item is: " + triple.Item3);
Tuples of Various Sizes
The Tuple is a generic type that can support simple tuples from 1 to 7 generic type arguments:
// a singleton tuple of integer
Tuple<int> x;
// or more
Tuple<int, double> y;
// up to seven
Tuple<int, double, char, double, int, string, uint> z;
Now, you may think that a Tuple of 1 is pretty
silly, but the main reason it exists is to support tuples beyond size 7,
where the tuples become nested. That is, if you want a Tuple beyond size 7, you have to make the 8th item a nested Tuple:
// an 8-tuple
Tuple<int, int, int, int, int, double, char, Tuple<string>> t8;
// an 9-tuple
Tuple<int, int, int, int, double, int, char, Tuple<string, DateTime>> t9;
// a 16-tuple
Tuple<int, int, int, int, int, int, int, Tuple<int, int, int, int, int, int, int, Tuple<int,int>>> t16;
Once constructed, you access the properties as with a simple Tuple but with one twist: the nested Tuple is accessed using the Rest property, and then you start over again with Item1...Item7 again:
// To access item 8 of t8
var eightItem = t8.Rest.Item1;
As you can see, dealing with large tuples can quickly become
cumbersome and difficult to read, so I generally don't recommend tuples
much beyond the 4 item range.
Tuples Simplify Treating Items as Unit
Probably the most powerful feature of tuples is that they let you
combine several elements into one unit that can be treated as a whole.
This makes it easy to return multiple values from a method (without
resorting to out parameters) or to pass multiple values
into a method where only one parameter is allowed. But where they
really shine, is treating the items together as a combined key:
// Dictionary of Order keyed on ID and Region
var ordersByIdAndRegion = new Dictionary<Tuplegt;int, string>>();
Or as the key in a GroupBy() clause in LINQ:
// Group all orders by ID and Region
var groupedOrders = orders.GroupBy(o => Tuple.Create(o.ID, o.Region));
Notice that again we could have created our own class here, but because hashing collections like Dictionary require a valid implementation of GetHashCode() and Equals(), you'd have to create your own and further, keep it well maintained:
public class IdAndRegionKey {
public int Id { get; set; }
public string Region { get; set; }
public override bool Equals(object other) {
var otherKey = other as IdAndRegionKey;
return otherKey != null
? otherKey.Id == Id && otherKey.Region == Region
: false;
}
public override int GetHashCode() {
int hash = 23;
hash += 17 * hash + (Region ?? string.Empty).GetHashCode();
hash += 17 * hash + Id.GetHashCode();
}
}
Notice how much work we had to do to build such a simple type to be a combination key for our Dictionary!
First of all, this is a lot of typing for something that has such a
small scope. Second, it's harder to maintain because you'd have to make
sure to implement (and keep updated) the Equals() and GetHashCode() method.
Sure, you could do something like the code above, but why go through the trouble for such a small-scoped type when Tuple will do it for you.
Tuple Factory Methods
So, now you've seen how to explicitly create a Tuple using new, such as this statement where we explicitly state the Tuple will contain an int and a string:
var double = new Tuple<int, string>(42, "The answer");
But there is another way! There is a static, non-generic class named Tuple that provides factory methods so that you can construct tuples using type inference:
var double = Tuple.Create(42, "The answer");
So the factory method above determines at compile-time that 42 is an int and "The answer" is a string and constructs the appropriate Tuple to house it. This will work for all the way up to octuples.
Tuple in a Nutshell
As you can see, there is great power (and potential for great abuse) in a Tuple. The Tuple
allows you to quickly construct a type that contains several items and
treats them as a unit. These instances are immutable and can be
constructed either directly or via factory methods in the Tuple static class. Finally, the Tuple already has an appropriate GetHashCode() and Equals() method overrides, which allows you to use it safely in LINQ or as a key to a hashing collection such as Dictionary or HashSet.
On the downside, the Item1, Item2, etc. property names are not very descriptive and large tuples can quickly become difficult to maintain if used explicitly.
Anonymous Types
Anonymous types are similar to Tuple yet very
different. Both of these types are meant to attack the problem of
producing quick and safe types of limited scope, but each attacks the
problem differently.
The Tuple gives you, for all intents and purposes, a
well-defined type that can be returned from methods and thus returned
from methods, used in other files, etc. But in consequence, it is
defined extremely generically by the types it contains and no more than
that. This means that every Tuple looks the same to .NET regardless of whether the first is a pair of order IDs and the second is a pair of days of the month.
Anonymous types, on the other hand, define a type at the point of creation that is much more descriptive. Where a Tuple has only a very generic set of Item... properties, the anonymous type has a well defined name and order to its properties.
Creatin an Anonymous Type
Anonymous types are constructed using the object initializer syntax.
The main differences being that the type-name is omitted and that the
properties must be assigned a value with an inferrable type.
For example, we could use anonymous typing to define a few points:
// same names, types, and order
var point1 = new { X = 13, Y = 7 };
var point2 = new { X = 5, Y = 0 };
Notice that we name each property and assign it a value, and that we
can infer the type of the property from the value. We can then access
these fields by name:
Console.WriteLine("My point is: ({0},{1})", point1.X, point1.Y);
The Type Definition
Notice that the two points above have the same property names, same
types, and in the same order. This means that (for the most part) they
are considered the same type. But change the property name, type, or
order, and the types will be considered different:
var point3 = new { Y = 3, X = 5 }; // different order
var point4 = new { X = 3, Y = 5.0 }; // different type for Y
var point5 = new {MyX = 3, MyY = 5 }; // different names
var point6 = new { X = 1, Y = 2, Z = 3 }; // different count
Also, remember that we said that the type must be inferable from the
assignment, this means that the following expressions are not allowed:
// Null can't be used directly. Null reference of what type?
var cantUseNull = new { Value = null };
In the code above, null could be any reference type,
so the anonymous type definition won't work. Thus, in cases like this,
you could make it work if you cast null to for an explicit type:
// You can use null if you cast to a type
var canUseNullWithCast = new { Value = (string) null };
Now, the type can be inferred and the anonymous type is happy.
Speaking of inferrence, you can even infer the property names in many
cases. Obviously, this won't work if you are assigning a literal, but
if you are assigning a property from a named variable or other property
and don't specify a new property name, it will take on the old name by
default:
int variable = 42;
// creates two properties named varriable and Now
var implicitProperties = new { variable, DateTime.Now };
In the example above, we've created an anonymous type that has a property named variable and a property named Now. However, if you were trying to do this with 42 directly:
var fail = new { 42, DateTime.Now };
It would fail at compile time because 42 is a literal and not a named identifier. Similarly, you can't use expressions such as
variable + x
since that is an expression and not a named identifier.
The Built-in Overrides on Anonymous Types
Once again, like Tuple anonymous types have overridden the Equals() and GetHashCode() for us, which lets us use them in GroupBy()
expressions and other similar situations. This again makes them a
great choice for grouping together items for a complex key in a LINQ
expression:
// group the transactions based on an anonymous type with properties UserId and Date:
byUserAndDay = transactions
.GroupBy(tx => new { tx.UserId, tx.At.Date })
.OrderBy(grp => grp.Key.Date)
.ThenBy(grp => grp.Key.UserId);
In fact, this can be much nicer because the Key of the grouping will have the UserId and Date property names, which is much easier to read than Item1 and Item2:
foreach (var group in byUserAndDay)
{
// the group’s Key is an instance of our anonymous type
Console.WriteLine("{0} on {1:MM/dd/yyyy} did:", group.Key.UserId, group.Key.Date);
// each grouping contains a sequence of the items.
foreach (var tx in group)
{
Console.WriteLine("\t{0}", tx.Amount);
}
}
One thing we lose with anonymous types is the ability to easily use
it beyond the declared scope. That is, if you declare an anonymous type
inside a method, it can be difficult to return it as a result or pass
it to a new method without type inference.
Consider, What if you wanted to return the results of the grouping above from a method? How would you define the return type?
Dictionary<???, Order> orders = ...;
You see the dilemma? How would you pass an anonymous type outside of a method it is used in? Sure, you could pass it out as object but then it would be difficult to cast back (though you could use dynamic to access the properties again of course).
The Anonymous Types in a Nutshell
Anonymous types give you a lot of power to define robust, short-lived
types at the point they are required. These instances are immutable
and have more meaningful property names than the Tuple which can make them easier to consume. Like the Tuple, anonymous types have complete overrides for GetHashCode() and Equals() which makes them suitable for hashing and equality checks.
The main downfall of anonymous types is that they are not intended
for use beyond their immediate scope, which means that in cases where
you want to return the results of a query from a method, you'd probably
want to define a real class.
Summary
Both tuples and anonymous types have their uses. Favor Tuples when
the type you need a small type that must live beyond its immediate
scope, but for which a full class is overkill. Such uses could be
passing a pair of items into a thread start method or into a state
variable, or perhaps using them as a combined key for a dictionary.
Anonymous types, on the other hand, are perfect for those cases where
you want a very short-lived type (no more than the current scope) to be
defined with meaningful names.
Do not think that you should always favor one or the other, consider what each excels at and choose them accordingly.
Comments
Post a Comment