Instances of the IEnumerable interface (either generic or non-generic) are termed sequences. This is a term which has gained increasing significance with the advent of iterator blocks and, more recently, LINQ. As the most primitive form in which an aggregation of objects can be represented, sequences have diverse uses and are the bread and butter of general-purpose code that operates on multiple objects at a time. They can represent static or dynamic sets of objects which can be either persistent or procedural in origin. A sequence can be anything from an array to a list to a random number generator. By operating on sequences in their most primitive form, we can completely abstract their implementation and write code which regards them as equivalents;
// property which returns a sequence used below
public IEnumerable<int> RandomNumbers {
get {
Random rand = new Random(0); // ensure sequence is deterministic
while (true) yield return rand.Next();
}
}
// demonstrates how the LINQ operators can work on many different sequence types
public void QuerySequences() {
// this sequence is a list, but we don't care about that
List cheeses = new List() { "feta", "brie", "camenbert", "cheddar", "edam" };
Console.WriteLine(cheeses.Where((x) => x.Length > 4).First());
// this sequence is a finite range of integers
Console.WriteLine(Enumerable.Range(100, 200).Where((x) => x > 100).First());
// this sequence is an infinite set of random numbers
Console.WriteLine(RandomNumbers.Where((x) => x > 50).First());
}
Until LINQ and the concept of ‘deferred execution’ came along (which really just means that a sequence is dynamic and isn’t persisted in memory), we tended to regard sequences as being static and persistent. As a result, we used them to very little effect, aside from the all important foreach statement (which was the original use case for IEnumerable). The way we use sequences has changed a lot; the result of a deferred LINQ query can be thought of as a deeply-nested set of iterator blocks, with each progression through the sequence triggering a progression through the inner sequence, and each progression through the inner sequence triggering a progression through the next nested sequence, and so on. This can be quite a concept to grasp when initially working with LINQ. However, while the way we use sequences has changed, the way we define them still represents a great many opportunities for innovation.
Concatenated Sequences
The Concat<> operator in LINQ does far more than simply join two sequences together. As it is a deferred operator, it returns a dynamic iterator that always represents the concatenation of the two (or however many you want) original sequences. In this way, the concatenation can be used much like a view can be used in a database:
// define two independent lists
List<string> first = new List<string>() { "apple", "orange", "grape", "peach", "nectarine" };
List<string> second = new List<string>() { "bacon", "beef", "chicken", "pancetta" };
// concatenate the two lists and output the contents
IEnumerable<string> wrapped = first.Concat(second);
Console.WriteLine(wrapped.Aggregate((x, y) => String.Format("{0}, {1}", x, y)));
// alter the contents of the original lists
first.Remove("orange");
second.Insert(2, "pork");
// output the contents - the changes are reflected in the output
Console.WriteLine(wrapped.Aggregate((x, y) => String.Format("{0}, {1}", x, y)));
This means that we can alter, reorder or even clear either of the original lists and still expect our changes to be reflected in the concatenated sequence. More than that, we can pass the object representing the concatenation to other methods or components – the linkage will always be preserved and we will never have to update or synchronise the sequences.
The Bigger Picture
So, let’s say that we have an object – the result of a combination of deferred LINQ operators – that represents a view on our original data. Recall that this data might come from a single location, or possibly dozens of different, independent sequences. Wouldn’t it be great if we could combine this “view” with the power of data-binding? Well, great news! We can. Data-binding requires an instance of IList (again, either generic or non-generic) to use as a data source, but the flexibility of the .NET Framework’s definition of a “list” is broad enough to make it easy to wrap a sequence with the functionality provided by IList.
Important: In this context, i’m talking about binding to something analogous to a database view. This means one-way (read-only) data-binding. Although it would be possible to implement a read/write IList wrapper for a sequence, it would be an inefficient solution.
After defining a wrapper class called ListSequenceWrapper<T>, which implements IList<T> and references the sequence via the property InnerSequence, we implement the required functionality as follows:
With this class at our disposal, we could easily bind our earlier concatenated sequence to a Windows Forms ComboBox control:
(assumes a BindingSource component has been associated with the ComboBox and the form also contains a Button control)
List<string> first = new List<string>() { "apple", "orange", "grape", "peach", "nectarine" };
List<string> second = new List<string>() { "bacon", "beef", "chicken", "pancetta" };
ListSequenceWrapper<string> wrapped = new ListSequenceWrapper<string>(first.Concat(second));
myBindingSource.DataSource = wrapped;
myButton.Click += (x, y) => {
first.Remove("orange");
second.Insert(2, "pork");
myBindingSource.DataSource = null; // force re-binding
myBindingSource.DataSource = wrapped;
};
Other Applications
In case the possibilities aren’t immediately obvious, here are some potential applications for data-binding to a dynamic/deferred sequence:
- A concatenation of a sequence of folders and a sequence of files would allow you to apply a sort to both sequences while maintaining folder-first order. Binding to the concatenation would remove the need to store and maintain the union in a separate collection.
- Binding to a procedural-style sequence (such as Enumerable.Range) would make it possible to measure the performance of data-binding to a long list without occupying a lot of memory.
- The drop-down portion of a ComboBox may contain both static/hard-coded choices as well as dynamic items. Binding to a concatenated sequence would allow independent edits to one sequence while maintaining the integrity of the other.
- A user-interface may assign some significance to the order of the sequence it is bound to, while the order may be unimportant to the underlying sequence. Rationalising the sequence as a list avoids unnecessarily augmenting the data representation.
- It may be useful in some applications to bind to a non-deterministic data source. For example, a “Tip of the Day” control might be bound to a sequence that changes each time it is observed.
Final Words
If there is anything that readers should take away with them after reading this article, it’s the need to move away from thinking of IEnumerable as only defining sequences with a backing store, like IList or ICollection. There is immense power in using sequences that represent logical transformations of data, rather than physically-redundant copies. Utilising sequences in this way can reduce memory usage, streamline and beautify code, as well as tipping the balance between feasibility and infeasibility of a solution. If developers were to exploit sequences to their own ends as much as they do when using LINQ, I think we could see some very exciting code in future.
Source Code
ListSequenceWrapper.cs