
.NET 3.5+A LINQ Style Operator to Combine Sequences
In some situations it is necessary to combine two sequences of values, item-by-item, into a new enumerable sequence or collection. This article describes how to create an extension method that achieves this using generics.
Combining Sequences
There are some programming problems that require that you combine two sequences of elements on an item-by-item basis. For example, you may have two lists of values that you wish to add together or two sets of strings that you need to concatenate. In this article we will create an extension method of the IEnumerable<T> interface that provides this functionality, using syntax similar to that of the LINQ standard query operators.
The new extension method fulfils the following requirements:
- Items from two enumerable sequences can be combined, item-by-item, in the order in which they appear.
- The two sequences can be of differing types and the returned sequence can be of a third type.
- The combination of the pairs of values is controlled by the developer using a Func delegate or lambda expression. Any operation upon the two values is possible.
- The method works with sequences that are not equal in length. When a pair cannot be made the default value for the type of the missing item is used instead.
Creating the Project
To follow the examples, create a new console application in Visual Studio. When using the method in your own projects you can use any type of solution. In this case a console application will be used to test the code with minimal effort.
Creating the Class
The new method is an extension method of the IEnumerable<T> interface. To create an extension method we need a static class to hold it. Create a new class named, "IEnumerableExtensions" and modify the declaration to match the following code:
public static class IEnumerableExtensions
{
}
Creating the Extension Method
The Combine extension method is a generic method that accepts three arguments. The first two parameters are the IEnumerable<T> sequences that are to be combined. These are named seqA and seqB and have the type parameters A and B respectively. The third parameter is a Func delegate that works with the individual A and B items. As the returned sequence can be of any type, the delegate returns the generic type T. To support these three types, the Combine method therefore has three type parameters. However, when calling the method these will rarely be required, as the types will normally be inferred.
To create the method signature add the following to the IEnumerableExtensions class:
public static IEnumerable<T> Combine<A, B, T>(
this IEnumerable<A> seqA, IEnumerable<B> seqB, Func<A, B, T> func)
{
}
The first task that the method performs is to obtain an enumerator for each of the sequences passed to it. These are initialised within a using statement so that they are correctly disposed when no longer required. Add the following code to the method to set up the enumerators. NB: The two Boolean values are flags that indicate whether each of the two enumerators is exhausted. When a flag is true, the matching enumerator is known to have a current value.
bool hasValueA, hasValueB;
using (var iteratorA = seqA.GetEnumerator())
using (var iteratorB = seqB.GetEnumerator())
{
}
Next the method loops until both enumerators are exhausted. This occurs when neither of the two flags is true. It is possible for one sequence to be exhausted and the other to have remaining items that will be combined with default values.
To create the loop, add the following code within the code block of the using statement.
do
{
} while (hasValueA || hasValueB);
We can now repeatedly move the pointer forwards through both sequences by adding the following to the loop's code block. Note that the MoveNext method will return false if a sequence has no more items.
hasValueA = iteratorA.MoveNext();
hasValueB = iteratorB.MoveNext();
The final task is to combine the two items using the provided delegate. The if statement checks that at least one item is present before combining, so that an extra item is not returned. If items exist, variables a and b are initialised to represent the current value from the appropriate sequence, or a default value for the type if the sequence is exhausted. The two values are then combines using the delegate and the resultant value is returned. Note the use of the yield keyword that means that the method works as an iterator.
if (hasValueA | hasValueB)
{
A a = hasValueA ? iteratorA.Current : default(A);
B b = hasValueB ? iteratorB.Current : default(B);
yield return func(a, b);
}
11 November 2010