0

次のコードで、この一見奇妙な Do 動作の原因を誰か教えてもらえますか? OnNext ごとに Do ハンドラーが 1 回呼び出されることを期待します。

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 );
    }
}
4

2 に答える 2

3

ここでの動作は完全に正常です。その理由は、サブスクリプションが1つだけではなく、多数あるためです。また、クエリ内の2つのオブザーバブル間のデカルト積のため、他の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

ただし、最初の呼び出しを取得したとき、2番目のオブザーバブルはまだサブスクライブされていないOnNextため、クエリによって生成された値は取得されません。streamY

が2回目に呼び出された場合にのみOnNext、クエリから値を取得します。この値は、たまたま最初のOnNext値と2番目の値のペアになります。これで、への新しいサブスクリプションも作成さstreamYれ、次の値を待機します。

これでstreamX、シーケンスから次の値を待つことで最初の2つの値が得られました。したがって、OnNext(3)が呼び出されると、2つの結果が得られます。

これが発生するたびに、増加し続けるDo呼び出しの数を確認できます。doCountY

実際、この非常に単純なSelectManyクエリを考えると、式は次のようになります。

doCountY = n * (n - 1) / 2

したがって、を介しOnNextて生成された6つの値を使用すると、またはにdoCountY等しくなります。6 * 5 / 215

10個の値で実行すると、10 * 9 / 2または45値が得られます。

したがって、SelectMany実際には、あなたが思っているよりもはるかに多くのサブスクリプションを実行します。これが、サブスクリプションの指数関数的な急増を防ぐために、通常、それぞれ1つの値のみを生成するオブザーバブルをチェーンするためにのみ使用する理由です。

今は意味がありますか?

于 2012-10-26T10:50:28.987 に答える
1

エニグマティビティの十分に肉付けされた答えの余談として、ここで2つのことが起こっています:

  1. Enumerables のような Observable は遅延して構成します。列挙可能なクエリのコンビネータが列挙子を通過するまで評価されるとは思わないのと同じように、パイプラインにサブスクライブしているオブザーバーの数と同じ回数、そのパイプラインに対して評価されたすべてのコンビネータがコンシューマーに表示されることを期待する必要があります。 . 要するに、x <- stream, y <- streamすでに2回成功しています。

  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ます。実際に使用することになる Rx のほとんどは、 orやGroupByなどの他の演算子でより適切に表現される可能性があります。MergeZipJoin

あなたが今尋ねたものと非常によく似た質問があります: Is Reactive Extensions evaluates too many times?

これが Rx で予期される動作である理由については、少し議論があります。

于 2012-10-26T12:22:45.577 に答える