如何确保仅枚举/执行一次IObservable

时间:2017-11-10 06:55:09

标签: c# system.reactive

我正在写一个执行异步操作的ICommand并将结果发布到一个可观察的序列。结果应该是懒惰的 - 除非有人订阅结果,否则什么都不会发生。如果用户将其订阅处理为结果,则应该取消。我的代码(非常简化)通常有效。棘手的是,当调用Execute时,我希望异步操作只发生一次,即使结果有很多订阅者。我想我只需要在发布结果之前做一个Replay.RefCount。但这并不奏效。或者,当可观察函数快速完成时,至少它在我的测试中不起作用。第一个订户获得整个结果,包括完成消息,这导致发布的结果被处理,然后为第二个订户完全重新创建。我过去常常使用的一种方法是在执行功能结束时插入1个滴答延迟。这为第二个订户提供了足够的时间来获得结果。

这个黑客是否合法?我不确定它是如何工作的,或者它是否会在非测试场景中保持不变。

确保结果只被枚举一次的不那么简单的方法是什么?我认为可能有用的一件事是,当用户订阅结果时,我将结果复制到ReplaySubject并发布。但我无法弄清楚如何让它发挥作用。第一个订户应该在计算结果并将它们填充到ReplaySubject中时滚动,但第二个订阅者应该只看到ReplaySubject。也许这是某种自定义Observable.Create

public class AsyncCommand<T> : IObservable<IObservable<T>>
{
    private readonly Func<IObservable<T>> _execute;
    Subject<IObservable<T>> _results;

    public AsyncCommand(Func<IObservable<T>> execute)
    {
        _execute = execute;
        _results = new Subject<IObservable<T>>();
    }

    // This would be ICommand.Execute, but I've simplified here
    public void Execute() => _results.OnNext(
        _execute()
        .Delay(TimeSpan.FromTicks(1)) // Take this line out and the test fails
        .Replay()
        .RefCount());

    // Subscribe to the inner observable to see the results of command execution
    public IDisposable Subscribe(IObserver<IObservable<T>> observer) =>
        _results.Subscribe(observer);
}

[TestClass]
public class AsyncCommandTest
{
    [TestMethod]
    public void IfSubscribeManyTimes_OnlyExecuteOnce()
    {
        int executionCount = 0;
        var cmd = new AsyncCommand<int>(() => Observable.Create<int>(obs =>
        {
            obs.OnNext(Interlocked.Increment(ref executionCount));
            obs.OnCompleted();
            return Disposable.Empty;
        }));
        cmd.Merge().Subscribe();
        cmd.Merge().Subscribe();
        cmd.Execute();
        Assert.AreEqual(1, executionCount);
    }
}

以下是我尝试使用ReplaySubject的方法。它可以工作,但结果不会懒散地发布,订阅会丢失 - 将订阅部署到结果中不会取消操作。

public void Execute()
{
    ReplaySubject<T> result = new ReplaySubject<T>();
    var lostSubscription = _execute().Subscribe(result);
    _results.OnNext(result);
}

1 个答案:

答案 0 :(得分:1)

这似乎有效。

public void Execute()
{
    int subscriptionCount = 0;
    int executionCount = 0;
    var result = new ReplaySubject<T>();
    var disposeLastSubscription = new Subject<Unit>();
    _results.OnNext(Observable.Create<T>(obs =>
    {
        Interlocked.Increment(ref subscriptionCount);
        if (Interlocked.Increment(ref executionCount) == 1)
        {
            IDisposable copySourceToReplay = Observable
                .Defer(_execute)
                .TakeUntil(disposeLastSubscription)
                .Subscribe(result);
        }
        return new CompositeDisposable(
            result.Subscribe(obs),
            Disposable.Create(() =>
            {
                if (Interlocked.Decrement(ref subscriptionCount) == 0)
                {
                    disposeLastSubscription.OnNext(Unit.Default);
                }
            }));
    }));
}