如何根据Func <t> </t> </t>将IObservable <t>窗口/缓冲到块中

时间:2014-10-15 13:31:22

标签: c# system.reactive observable

给出一个课程:

class Foo { DateTime Timestamp {get; set;} }

...和IObservable<Foo>,保证单调增加 Timestamp,如何根据IObservable<IList<Foo>>生成Timestamp数据块。 1}} S'

即。每个IList<Foo>应该有五秒钟的事件,或者其他什么。我知道我可以Buffer使用TimeSpan超载,但我需要花时间从事件本身,而不是挂钟。 (除非有一种聪明的方式在IScheduler提供IObservable本身作为.Now的来源?)

如果我尝试使用Observable.Buffer(this IObservable<Foo> source, IObservable<Foo> bufferBoundaries)重载,请执行以下操作:

IObservable<Foo> foos = //...;
var pub = foos.Publish();
var windows = pub.Select(x => new DateTime(
    x.Ticks - x.Ticks % TimeSpan.FromSeconds(5).Ticks)).DistinctUntilChanged();

pub.Buffer(windows).Subscribe(x => t.Dump()));  // linqpad
pub.Connect();

...然后IList个实例包含导致窗口关闭的项目,但我真的希望这个项目进入下一个窗口/缓冲区。

E.g。使用时间戳[0, 1, 10, 11, 15],您将获得[[0], [1, 10], [11, 15]]而不是[[0, 1], [10, 11], [15]]

的块

3 个答案:

答案 0 :(得分:6)

这是一个想法。组密钥条件是&#34;窗口号&#34;我使用GroupByUntil。这为您提供了示例中所需的输出(我已经使用了一个int流,就像那个例子一样 - 但你可以用你需要的任何东西替换你的窗口编号)。

public class Tests : ReactiveTest
{
    public void Test()
    {
        var scheduler = new TestScheduler();
        var xs = scheduler.CreateHotObservable<int>(
            OnNext(0, 0),
            OnNext(1, 1),
            OnNext(10, 10),
            OnNext(11, 11),
            OnNext(15, 15),
            OnCompleted(16, 0));                  

        xs.Publish(ps =>                                // (1)
            ps.GroupByUntil(
                p => p / 5,                             // (2)
                grp => ps.Where(p => p / 5 != grp.Key)) // (3)
            .SelectMany(x => x.ToList()))               // (4)
        .Subscribe(Console.WriteLine);

        scheduler.Start();
    }
}

注释

  1. 我们发布源流,因为我们会多次订阅。
  2. 这是一个创建组密钥的功能 - 使用此功能从项目类型生成窗口编号。
  3. 这是组终止条件 - 使用它来检查另一个窗口中项目的源流。请注意,这意味着窗口不会关闭,直到其外部的元素到达,或源流终止。如果你考虑一下这很明显 - 你需要的输出需要在窗口结束后考虑下一个元素。请注意,如果您的源与实时有任何关系,您可以将其与Observable.Timer+Select合并,该Test()输出您的术语的空/默认实例以更早地终止该流。
  4. SelectMany将组放入列表并展平流。
  5. 如果你包含nuget包rx-testing,这个例子将很好地在LINQPad中运行。新建一个测试实例,然后运行{{1}}方法。

答案 1 :(得分:2)

WindowBuffer的概括,而GroupJoinWindow(和Join)的概括。当您撰写WindowBuffer查询时,您发现通知被错误地包含在窗口/列表的边缘或从窗口/列表的边缘排除,然后根据GroupJoin重新定义您的查询控制边缘通知到达的位置。

请注意,为了使关闭通知可用于新打开的窗口,您必须将边界定义为这些通知的窗口(窗口化数据,而不是边界数据)。在您的情况下,您不能使用DateTime值序列作为边界,您必须使用一系列Foo对象。为此,我已使用Select - &gt; DistinctUntilChanged - &gt; Scan查询替换了您的Where - &gt; Select查询。

var batches = foos.Publish(publishedFoos => publishedFoos
  .Scan(
    new { foo = (Foo)null, last = DateTime.MinValue, take = true },
    (acc, foo) =>
    {
      var boundary = foo.Timestamp - acc.last >= TimeSpan.FromSeconds(5);

      return new
      {
        foo,
        last = boundary ? foo.Timestamp : acc.last,
        take = boundary
      };
    })
  .Where(a => a.take)
  .Select(a => a.foo)
  .Publish(boundaries => boundaries
    .Skip(1)
    .StartWith((Foo)null)
    .GroupJoin(
      publishedFoos,
      foo => foo == null ? boundaries.Skip(1) : boundaries,
      _ => Observable.Empty<Unit>(),
      (foo, window) => (foo == null ? window : window.StartWith(foo)).ToList())))
  .Merge()
  .Replay(lists => lists.SkipLast(1)
                        .Select(list => list.Take(list.Count - 1))
                        .Concat(lists),
          bufferSize: 1);

只有当您希望序列最终结束并且您不关心不丢弃最后一个通知时,才需要最后的Replay查询;否则,您可以简单地将window.StartWith(foo)修改为window.StartWith(foo).SkipLast(1)以获得相同的基本结果,但最后一个缓冲区的最后通知将会丢失。

答案 2 :(得分:2)

我认为詹姆斯世界的答案更整洁/更具可读性,但对于后人,我发现了另一种使用Buffer()的方法:

IObservable<Foo> foos = //...;
var pub = foos.Publish();
var windows = pub.Select(x => new DateTime(
        x.Ticks - x.Ticks % TimeSpan.FromSeconds(5).Ticks))
    .DistinctUntilChanged().Publish.RefCount();

pub.Buffer(windows, x => windows).Subscribe(x => t.Dump()));
pub.Connect();

凭借10米赛事,詹姆斯的进近速度超过2.5倍(我的机器上20秒对56秒)。