Sequences, LINQ, Rx, & Reaqtor Part 3: Rx
To understand Reaqtor, it is necessary to understand Rx (the Reactive Extensions for .NET). And to understand Rx, it is necessary to understand how C# works with sequences of items. In this series I will outline the ideas at the heart of Reaqtor, and how they are handled in C#. So far we have looked at representing sequences of items with IEnumerable<T>
, its expression-tree-based cousin IQueryable<T>
, and processing such sequences with LINQ. Now we'll look at an alternative representation of the same abstraction.
A basic assumption made by both IEnumerable<T>
and IQueryable<T>
is that the information source (whether it's a data structure, an algorithm, or some sort of database) is able to provide information on demand. Code using these types asks for results either using foreach
, or a LINQ operator that evaluates an entire query such as ToList
or ToArray
.
However, there is another way to model sequences of things: the source might produce items when it is good and ready to. This is sometimes referred to as 'push' and it's the model Rx uses. It models this with an interface called IObservable<T>
.
The basic idea underpinning IObservable<T>
is identical to that of IEnumerable<T>
: both represent a sequence of items—one damn thing after another. The only difference is that the source gets to decide when the next item is produced.
The IObservable<T>
interface is a sort of mirror image of IEnumerable<T>
. Just as the latter has a corresponding IEnumerator<T>
interface used when iterating through the items, IObservable<T>
has a corresponding IObserver<T>
. But whereas with IEnumerable<T>
you ask for an IEnumerator<T>
when you're ready to start working through the sequence, with IObservable<T>
, you must supply an IObserver<T>
when you're ready for items, because the IObservable<T>
is going to invoke methods on you for each item.
IObserver<T>
has three methods: OnNext(T)
, which the source invokes for each item it produces, OnCompleted
, which it invokes to tell you the sequence has finished (the logical equivalent of IEnumerator<T>.MoveNext()
returning false
), and OnError
, which it invokes to report an error (the logical equivalent of the MoveNext()
method throwing an exception).
So enumerables and observables are logically equivalent (to the extent that the Rx libraries provide adapters that can convert from one to the other). It is just that sometimes, one will be a more natural way to represent some sequence than the other. IObservable<T>
is the more natural way to represent events.
Rx includes a complete LINQ provider. More than complete in fact: in addition to all the operators available in other LINQ providers, Rx adds several new ones. (The team also released Ix, the "Interactive Extensions for .NET" which essentially provides versions of all these additional operators for IEnumerable<T>
and IQueryable<T>
.) So given some source of events, you can filter them with a Where
clause, or do any of the other things LINQ supports.
Rx's 'push' world offers a particular distinction that we have also seen in the 'pull' world with IEnumerable<T>
vs IQueryable<T>
: in addition to IObservable<T>
, Rx also defines IQbservable<T>
(note the 'Q'). This is essentially a version of IObservable<T>
but one where all the LINQ operators take lambdas in the expression tree form (whereas for a regular IObservable<T>
the standard operators all take ordinary delegates—references to methods).
In the fourth part of this series, we'll take a look at Reaqtive.