Understanding Tuples in .NET 4.0 and .NET 2.0/3.0/3.5

.NET 4.0 introduces a new type, the Tuple. In mathematics, a tuple is an ordered set of elements, e.g. (1, 2, 3, 4, 5) is a 5-tuple. In programming (i.e. type theory) a tuple has a fixed size and the underlying type of its components is explicit.

With this in mind, we can understand tuples simply as a wrapper type of two or more values, e.g.:

var person = new Tuple(“Matt”, 25);
// person.Item1 …
// person.Item2
Whilst we wait for .NET 4.0 to come a’knocking, we can still capture this functionality with v2.0 of the framework, thanks to generics. We can define our own Tuple types, for instance:

namespace MyTuples
{
using System;
using System.Globalization;

/// <summary> /// Represents a finite 2-item tuple. /// </summary> /// <typeparam name="TFirst">The type of the first item.</typeparam> /// <typeparam name="TSecond">The type of the second item.</typeparam> public class Tuple<TFirst, TSecond> : IEquatable<Tuple<TFirst, TSecond>> { #region Constructors /// <summary> /// Initialises a new instance of <see cref="Tuple{TFirst,TSecond}" />. /// </summary> public Tuple() { } /// <summary> /// Initialises a new instance of <see cref="Tuple{TFirst,TSecond}" />. /// </summary> /// <param name="first">The first item.</param> public Tuple(TFirst first) { this.First = first; } /// <summary> /// Initialises a new instance of <see cref="Tuple{TFirst,TSecond}" />. /// </summary> /// <param name="first">The first item.</param> /// <param name="second">The second item.</param> public Tuple(TFirst first, TSecond second) { this.First = first; this.Second = second; } #endregion #region Properties /// <summary> /// Gets or sets the first item. /// </summary> public TFirst First { get; set; } /// <summary> /// Gets or sets the second item. /// </summary> public TSecond Second { get; set; } #endregion #region Methods /// <summary> /// Calculates the hash code for this tuple. /// </summary> /// <returns>The hash code for this tuple.</returns> private int CalculateHashCode() { int hashCode = 0; TFirst first = default(TFirst); TSecond second = default(TSecond); if (!object.Equals(this.First, first)) { hashCode += this.First.GetHashCode(); } if (!object.Equals(this.Second, second)) { hashCode += this.Second.GetHashCode(); } return hashCode; } /// <summary> /// Gets the hash code for this tuple. /// </summary> /// <returns>The hash code for this tuple.</returns> public override int GetHashCode() { return CalculateHashCode(); } /// <summary> /// Determines if this instance equals the specified tuple. /// </summary> /// <param name="other">The other tuple to compare to.</param> /// <returns>True if this instance equals the specified tuple, otherwise false.</returns> public bool Equals(Tuple<TFirst, TSecond> other) { if (other == null) { return false; } return (this.GetHashCode() == other.GetHashCode()); } /// <summary> /// Determines if the two instances equal. /// </summary> /// <param name="firstTuple">The first tuple to compare.</param> /// <param name="secondTuple">The second tuple to compare.</param> /// <returns>True if the two instances are equal, otherwise false.</returns> public static bool Equals(Tuple<TFirst, TSecond> firstTuple, Tuple<TFirst, TSecond> secondTuple) { if (firstTuple == null && secondTuple == null) { return true; } if (firstTuple == null || secondTuple == null) { return false; } return firstTuple.Equals(secondTuple); } /// <summary> /// Determines if this instance equals the specified object. /// </summary> /// <param name="obj">The other object to compare to.</param> /// <returns>True if this instance equals the specified object, otherwise false.</returns> public override bool Equals(object obj) { if (obj == null) { return false; } if (obj is Tuple<TFirst, TSecond>) { return Equals(obj as Tuple<TFirst, TSecond>); } Type first = typeof(Tuple<TFirst, TSecond>); Type second = obj.GetType(); throw new NotSupportedException( string.Format(CultureInfo.CurrentUICulture, "Cannot check for equality between types '{0}' and '{1}'.", first.Name, second.Name)); } #endregion }

}
But where would this be any use? We’ve all seen this before:

public Result DoSomething() {
return new Result
{
Name = “Matt”,
Age = 25
};
}
…and perhaps the even more face aching:

public void DoSomething(out string name, out int age) {
name = “Matt”;
age = 25;
}
Often is the case when you have a function that you want to return multiple values, you resort to either one of the methods described above. This often results in your class library expanding rapidly due to the large number of these wrapper types who’s only purpose is to contain other types.

We could in fact solve this issue using a Tuple:

public Tuple DoSomething() {
return new Tuple(“Matt”, 25);
}
Now, you could go one step further…how about an extension method that joins enumerations of types into Tuples?

namespace MyTuples
{
using System;
using System.Collections.Generic;
using System.Linq;

static class IEnumerableExtensions { #region Methods /// <summary> /// Converts the given enumerations to 2-part tuples. /// </summary> /// <typeparam name="TFirst">The type of the first item in the tuple.</typeparam> /// <typeparam name="TSecond">The type of the second item in the tuple.</typeparam> /// <param name="items">The first set of items.</param> /// <param name="secondItems">The second set of items.</param> /// <param name="includeUnmatched">Should we include unmatched items?</param> /// <returns>An enumeration of 2-part tuples created from the given sets.</returns> public static IEnumerable<Tuple<TFirst, TSecond>> ToTuples<TFirst, TSecond>( this IEnumerable<TFirst> items, IEnumerable<TSecond> secondItems, bool includeUnmatched) { if (items == null) { throw new ArgumentNullException("items"); } if (secondItems == null) { throw new ArgumentNullException("secondItems"); } TFirst[] firstArray = items.ToArray(); TSecond[] secondArray = secondItems.ToArray(); IList<Tuple<TFirst, TSecond>> results = new List<Tuple<TFirst, TSecond>>(); int i = 0; for (; i < firstArray.Length; i++) { if (i < secondArray.Length) { results.Add(new Tuple<TFirst, TSecond>(firstArray[i], secondArray[i])); } else if (includeUnmatched) { results.Add(new Tuple<TFirst, TSecond>(firstArray[i], default(TSecond))); } } if (includeUnmatched && (i < secondArray.Length)) { for (; i < secondArray.Length; i++) { results.Add(new Tuple<TFirst, TSecond>(default(TFirst), secondArray[i])); } } return results; } /// <summary> /// Converts the given enumerations to 2-part tuples. /// </summary> /// <typeparam name="TFirst">The type of the first item in the tuple.</typeparam> /// <typeparam name="TSecond">The type of the second item in the tuple.</typeparam> /// <param name="items">The first set of items.</param> /// <param name="secondItems">The second set of items.</param> /// <returns>An enumeration of 2-part tuples created from the given sets.</returns> public static IEnumerable<Tuple<TFirst, TSecond>> ToTuples<TFirst, TSecond>( this IEnumerable<TFirst> items, IEnumerable<TSecond> secondItems) { return ToTuples(items, secondItems, false); } #endregion }
Code language: PHP (php)

}
What these methods allow us to do is create Tuples from each of the items. These examples are only using 2-tuple instances, but you can create Tuples and extension methods for however many types you want to wrap.

Given the above code, I could then do something like this:

string[] names = { “Matt”, “Andy”, “Chris” };
int[] ages = { 25, 30, 28 };

var people = names.ToTuples(ages);
foreach (var person in people)
{
Console.WriteLine(“Name: {0}, Age: {1}”, person.First, person.Second);
}
Thanks to type inference, we don’t need to explictly state the types of the items in the tuples, the compiler will handle this for us. Nicey nicey eh?

Leave a Reply

Your email address will not be published. Required fields are marked *

Related Post