反应性扩展操作员触发的次数比我预期的要多

时间:2012-10-26 09:39:57

标签: system.reactive

任何人都可以告诉我在下面的代码中导致这个看似奇怪的Do行为的原因是什么?我希望每个OnNext都可以调用一次Do处理程序。

using System;
using System.Reactive.Linq;
using System.Reactive.Subjects;
using NUnit.Framework;

[TestFixture]
public class WhyDoesDoActSoWierd
{
    [Test]
    public void ButWhy()
    {
        var doCount = 0;
        var observable = new Subject<int>();
        var stream = observable.Do( x => doCount++ );
        var subs =
            (from x in stream
             where x % 2 == 1
             from y in stream
             where y % 2 == 0
                   && y == x + 1
             select new { x, y })
                .Subscribe( x => Console.WriteLine( "{0}, {1}", x.x, x.y ) );

        observable.OnNext( 1 );
        // doCount == 1
        observable.OnNext( 2 );
        // doCount == 3
        observable.OnNext( 3 );
        // doCount == 5
        observable.OnNext( 4 );
        // doCount == 8
        observable.OnNext( 5 );
        // doCount == 11
        observable.OnNext( 6 );
        // doCount == 15
        Assert.AreEqual( 6, doCount );
    }
}

2 个答案:

答案 0 :(得分:3)

这里的行为完全正常。原因是你不只是有一个订阅,你有很多。而且由于查询中两个可观察对象之间存在笛卡尔积,因此Do的数量比您预期的要多。

让我们看看你的另一个(但是类似的)查询。

    var doCountX = 0;
    var doCountY = 0;

    Action dump = () =>
        Console.WriteLine("doCountX = {0}, doCountY = {1}", doCountX, doCountY);

    var observable = new Subject<int>();

    var streamX = observable.Do(x => doCountX++);
    var streamY = observable.Do(x => doCountY++);

    var query =
        from x in streamX
        from y in streamY
        select new { x, y };

    query.Subscribe(z => Console.WriteLine("{0}, {1}", z.x, z.y));

    dump();
    for (var i = 1; i <= 6; i++)
    {
        observable.OnNext(i);
        dump();
    }

这个输出是:

doCountX = 0, doCountY = 0
doCountX = 1, doCountY = 0
1, 2
doCountX = 2, doCountY = 1
1, 3
2, 3
doCountX = 3, doCountY = 3
1, 4
2, 4
3, 4
doCountX = 4, doCountY = 6
1, 5
2, 5
3, 5
4, 5
doCountX = 5, doCountY = 10
1, 6
2, 6
3, 6
4, 6
5, 6
doCountX = 6, doCountY = 15

doCountX = 0, doCountY = 0的初始转储是预期的,因为在dump()的任何调用发生之前,此OnNext的调用都会发生。

但是当我们第一次调用OnNext时,我们没有得到查询产生的值,因为第二个streamY observable尚未订阅。

只有在第二次调用OnNext时,才会从查询中获取一个值,该值恰好是与第二个值配对的第一个OnNext值。现在,这也会创建一个新的streamY订阅,等待下一个值。

所以我们现在从streamX得到前两个值,等待序列中的下一个值。因此,当调用OnNext(3)时,我们会得到两个结果。

每次发生这种情况时,您都可以看到Do来电增加doCountY的电话数量不断上升。

事实上,鉴于这个非常简单的SelectMany查询,公式为:

doCountY = n * (n - 1) / 2

因此,通过OnNext生成6个值,doCountY等于6 * 5 / 215

使用10个值运行会提供10 * 9 / 245值。

所以SelectMany实际上比你想象的要多得多订阅。这通常是为什么你通常只会用它来链接一起只能产生单个值的观察者,以防止订阅的指数性爆炸。

现在有意义吗?

答案 1 :(得分:1)

除了Enigmativity的充实答案之外,还有两件事情在这里:

  1. 像Enumerables这样的可观察者懒洋洋地构成。 就像你不希望在通过枚举器进行评估之前评估可枚举查询中的任何组合子一样,你应该期望消费者看到为该管道评估的所有组合器,就像订阅管道的观察者数量一样多。 。 简而言之,x <- stream, y <- stream已经两次。

  2. 理解被重写为:

        stream1.Where(x => x % 2 == 1)
               .SelectMany(x => stream2
                                .Where(y => y % 2 == 0 && y == x + 1)
                                .Select(y => new { x, y })
                                );
    

    对于收到的x的每个值,您将订阅与谓词匹配的流的所有值 - 这将变得很多。查询理解通常会被解除为SelectMany / Join / GroupBy - 您实际上最终使用的大部分Rx可能会更好地表达为其他运算符 - 例如{{ 1}}或Merge或甚至Zip

  3. 有一个问题与你刚刚提出的问题非常相似: 的 Is Reactive Extensions evaluating too many times?

    有一个关于为什么这是Rx中预期行为的讨论。