我正在写一个执行异步操作的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);
}
答案 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);
}
}));
}));
}