A pair of Subscribe and Unsubscribe methods would be non-compositional. Every operator would need to keep a dictionary of observers that were passed in to Subscribe, mapping those onto each observer instance that was passed to dependent observable sequences (passed in to the operator).
For example, consider writing a Merge operator for two sources. Today, this looks pretty much like this (textarea compiled):
static IObservable<T> Merge<T>(IObservable<T> xs, IObservable<T> ys)
{
return Observable.Create<T>(observer =>
{
var n = 2;
var mergeObserver = Observer.Create<T>(
observer.OnNext,
observer.OnError,
() =>
{
// protected by the gate, see use of Synchronize below
if (--n == 0)
observer.OnCompleted();
}
);
var gate = new object();
return new CompositeDisposable(
xs.Synchronize(gate).Subscribe(mergeObserver),
ys.Synchronize(gate).Subscribe(mergeObserver)
);
});
}
As you can see, composition of sequences also leads to composition of the IDisposable objects returned from the Subscribe calls. Notice there's a lot of stuff going on in Observable.Create that automatically disposes the returned IDisposable upon sending a terminal message to the given observer. In this case, calls to observer.OnError and observer.OnCompleted take care of disposing both subscriptions in the CompositeDisposable. (But that's a whole different subject to talk about some time.)
The code below is hypothetical, assuming the existence of Subscribe/Unsubscribe pairs on IObservable (hence with a Create factory method that has two actions):
static IObservable<T> Merge<T>(IObservable<T> xs, IObservable<T> ys)
{
var map = new Dictionary<IObserver<T>, IObserver<T>>();
return Observable.Create<T>(
subscribe: observer =>
{
var gate = new object();
var n = 2;
var mergeObserver = Observer.Create<T>(
x =>
{
lock (gate)
observer.OnNext(x);
},
ex =>
{
lock (gate)
observer.OnError(ex);
},
() =>
{
lock (gate)
if (--n == 0)
observer.OnCompleted();
}
);
//
// Using .Synchronize(gate) would be a mess, because then we need to
// keep the two synchronized sequences around as well, such that we
// can call Unsubscribe on those. So, we're "better off" inlining the
// locking code in the observer.
//
// (Or: how composition goes down the drain!)
//
xs.Subscribe(mergeObserver);
ys.Subscribe(mergeObserver);
lock (map)
map[observer] = mergeObserver;
},
unsubscribe: observer =>
{
var mergeObserver = default(IObserver<T>);
lock (map)
map.TryGetValue(observer, out mergeObserver);
if (mergeObserver != null)
{
xs.Unsubscribe(mergeObserver);
ys.Unsubscribe(mergeObserver);
}
}
);
}
Beware this is hypothetical; I haven't even thought about more edge cases, nor how this Create would work in order to clean up after itself upon a call to OnError or OnCompleted. Also, with Merge as an example we're lucky we don't have other resources to care about during "Unsubscribe" (e.g. scheduler jobs).
Hope this helps,
-Bart (Rx team)