重放值或错误的惰性可观察序列

时间:2016-02-10 12:33:24

标签: c# .net system.reactive

我正在尝试创建一个具有以下特征的可观察管道:

  • 很懒(在有人订阅之前什么都不做)
  • 无论收到多少订阅,
  • 最多执行一次
  • 重播其结果值,如果有任何OR
  • 重播其产生的错误(如果有)

对于我的生活,我无法弄清楚正确的语义来实现这一目标。我认为做这样的事情很简单:

Observable
    .Defer(() => Observable
        .Start(() => { /* do something */ })
        .PublishLast()
        .ConnectUntilCompleted());

ConnectUntilCompleted只是听起来像这样:

public static IObservable<T> ConnectUntilCompleted<T>(this IConnectableObservable<T> @this)
{
    @this.Connect();
    return @this;
}

当observable成功终止时,这似乎有效,但是当出现错误时则不行。任何订阅者都不会收到错误:

[Fact]
public void test()
{
    var o = Observable
        .Defer(() => Observable
            .Start(() => { throw new InvalidOperationException(); })
            .PublishLast()
            .ConnectUntilCompleted());

    // this does not throw!
    o.Subscribe();
}

谁能告诉我我做错了什么?为什么Publish不重播它收到的任何错误?

更新:它变得更加陌生:

[Fact]
public void test()
{
    var o = Observable
        .Defer(() => Observable
            .Start(() => { throw new InvalidOperationException(); })
            .PublishLast()
            .ConnectUntilCompleted())
        .Do(
            _ => { },
            ex => { /* this executes */ });

    // this does not throw!
    o.Subscribe();

    o.Subscribe(
        _ => { },
        ex => { /* even though this executes */ });
}

3 个答案:

答案 0 :(得分:3)

试试这个版本的ConnectUntilCompleted方法:

public static IObservable<T> ConnectUntilCompleted<T>(this IConnectableObservable<T> @this)
{
    return Observable.Create<T>(o =>
    {
        var subscription = @this.Subscribe(o);
        var connection = @this.Connect();
        return new CompositeDisposable(subscription, connection);
    });
}

允许Rx正常运行。

现在我已添加它以帮助显示正在发生的事情:

public static IObservable<T> ConnectUntilCompleted<T>(this IConnectableObservable<T> @this)
{
    return Observable.Create<T>(o =>
    {
        var disposed = Disposable.Create(() => Console.WriteLine("Disposed!"));
        var subscription = Observable
            .Defer<T>(() => { Console.WriteLine("Subscribing!"); return @this; })
            .Subscribe(o);
        Console.WriteLine("Connecting!");
        var connection = @this.Connect();
        return new CompositeDisposable(disposed, subscription, connection);
    });
}

现在您的观察结果如下:

var o =
    Observable
        .Defer(() =>
            Observable
                .Start(() =>
                {
                    Console.WriteLine("Started.");
                    throw new InvalidOperationException();
                }))
        .PublishLast()
        .ConnectUntilCompleted();

最后一个关键是实际处理订阅中的错误 - 因此仅仅执行o.Subscribe()是不够的。

这样做:

        o.Subscribe(
            x => Console.WriteLine(x),
            e => Console.WriteLine(e.Message),
            () =>  Console.WriteLine("Done."));

        o.Subscribe(
            x => Console.WriteLine(x),
            e => Console.WriteLine(e.Message),
            () =>  Console.WriteLine("Done."));

        o.Subscribe(
            x => Console.WriteLine(x),
            e => Console.WriteLine(e.Message),
            () =>  Console.WriteLine("Done."));         

当我跑步时,我得到了这个:

Subscribing!
Connecting!
Subscribing!
Connecting!
Subscribing!
Connecting!
Started.
Operation is not valid due to the current state of the object.
Disposed!
Operation is not valid due to the current state of the object.
Disposed!
Operation is not valid due to the current state of the object.
Disposed!

请注意,“已启动”仅出现一次,但会报告错误三次。

(有时Started在第一次订阅后出现在列表中较高位置。)

我认为这是你想要的描述。

答案 1 :(得分:2)

为了支持@Engimativity的回答,我想展示你应该如何运行你的测试,以便你不再得到这些&#34;惊喜&#34;。您的测试是非确定性的,因为它们是多线程/并发的。在未提供Observable.Start的情况下使用IScheduler会产生问题。如果您使用TestScheduler运行测试,那么您的测试现在将是单线程且确定性的

[Test]
public void Test()
{
    var testScheduler = new TestScheduler();
    var o = Observable
        .Defer(() => Observable
            .Start(() => { throw new InvalidOperationException(); }, testScheduler)
            .PublishLast()
            .ConnectUntilCompleted());

    var observer = testScheduler.CreateObserver<Unit>();
    o.Subscribe(observer);

    testScheduler.Start();

    CollectionAssert.IsNotEmpty(observer.Messages);
    Assert.AreEqual(NotificationKind.OnError, observer.Messages[0].Value.Kind);
}

答案 2 :(得分:0)

实现您的要求的另一种方法可能是:

var lazy = new Lazy<Task>(async () => { /* execute once */ }, isThreadSafe: true);
var o = Observable.FromAsync(() => lazy.Value);

第一次订阅时,lazy将创建(并执行)任务。对于其他订阅,lazy将返回相同(可能已经完成或失败)的任务。