BlackWaspTM

This web site uses cookies. By using the site you accept the cookie policy.This message is for compliance with the UK ICO law.

LINQ
.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);
}

Testing the Combine Method

We can test the Combine method by calling it from the Main method of the console application. First, let's combine two arrays of integers by summing the values. In this case the two sequences are the same length. Note the use of the lambda expression with two parameters to represent items from each sequence and add them together.

int[] integers1 = new int[] { 1, 2, 3, 4, 5 };
int[] integers2 = new int[] { 10, 20, 30, 40, 50 };

var sums = integers1.Combine(integers2, (i, j) => i + j);

// 11, 22, 33, 44, 55

We can now try a more complicated example. In the test below, one of the arrays of integers is combined with an array of characters. The resultant sequence contains a set of strings, each made by concatenating the input values. Note that the last item combines "F" and zero. This is because the series of characters is longer than the integer array and, therefore, the default integer value is used when combining with the final character.

char[] characters = new char[] { 'A', 'B', 'C', 'D', 'E', 'F' };

var items = characters.Combine(integers1, (c, i) => string.Format("{0}{1}", c, i));
Console.WriteLine(items.Aggregate((a, i) => (a + ", " + i)));

// A1, B2, C3, D4, E5, F0

A Note on Exceptions

The Combine method described does not check the input sequences to ensure that they are not null. This means that an exception could be thrown by the method but that this exception will be deferred until the combined sequence is first accessed. To mirror true standard query operators, the method should be updated to eagerly check for null sequences, as described in "Custom LINQ Operators, Deferred Execution and Exceptions".

11 November 2010