在可活动扩展中对可观察的IEnumerable <t>流进行分区并延迟

时间:2017-05-17 15:28:39

标签: c# system.reactive

我有以下Rx扩展方法来分区IEnumerable&lt; T&gt;并延迟每个分区值的产生。它使用IEnumerable&lt; T&gt;分区数据的扩展,也用单元测试显示。

是否有更好的方法来延迟&#39;而不是使用Observable.Timer()。Wait()方法调用?

public static class RxExtensions
{
    public static IObservable<IEnumerable<T>> PartitionWithInterval<T>(this IObservable<IEnumerable<T>> source, int size, TimeSpan interval, IScheduler scheduler = null)
    {
        if (scheduler == null)
        {
            scheduler = TaskPoolScheduler.Default;
        }

        var intervalEnabled = false;
        return source.SelectMany(x => x.Partition(size).ToObservable())
            .Window(1)
            .SelectMany(x =>
            {
                if (!intervalEnabled)
                {
                    intervalEnabled = true;
                }
                else
                {
                    Observable.Timer(interval, TaskPoolScheduler.Default).Wait();
                }

                return x;
            })
            .ObserveOn(scheduler);
    } 
}

public static class EnumerableExtensions
{
    public static IEnumerable<IEnumerable<T>> Partition<T>(this IEnumerable<T> source, int size)
    {
        using (var enumerator = source.GetEnumerator())
        {
            var items = new List<T>();
            while (enumerator.MoveNext())
            {
                items.Add(enumerator.Current);
                if (items.Count == size)
                {
                    yield return items.ToArray();

                    items.Clear();
                }
            }

            if (items.Any())
            {
                yield return items.ToArray();
            }
        }
    }
}

Rx扩展方法的测试如下所示:

static void Main(string[] args)
{
     try
     {
         var data = Enumerable.Range(0, 10);
         var interval = TimeSpan.FromSeconds(1);

         Observable.Return(data)
            .PartitionWithInterval(2, interval)
            .Timestamp()
            .Subscribe(x =>
                {
                   var message = $"{x.Timestamp} - count = {x.Value.Count()}, values - {x.Value.First()}, {x.Value.Last()}";
                   Console.WriteLine(message);
                });

           Console.ReadLine();
       }
       catch (Exception e)
       {
           Console.WriteLine(e);
       }
}

3 个答案:

答案 0 :(得分:1)

这应该这样做:

public static IObservable<IEnumerable<T>> PartitionWithInterval<T>(this IObservable<IEnumerable<T>> source, int size, TimeSpan interval, IScheduler scheduler = null)
{
    if (scheduler == null)
    {
        scheduler = TaskPoolScheduler.Default;
    }

    return source
        //don't need the .ToObservable() call, since Zip can work on IEnumerable + IObservable.
        .SelectMany(x => x.Partition(size)) 
        .Zip(Observable.Interval(interval, scheduler).StartWith(0), (x, _) => x)
        .ObserveOn(scheduler);
}

有趣PartitionWithInterval实际调用PartitionInterval

StartWith就在那里,因此您可以立即删除分区:类似于intervalEnabled标记的方式。

答案 1 :(得分:0)

感觉你必须使用运算符Buffer。试试这个:

            data.ToObservable()
                .Buffer(2)
                .Zip(Observable.Interval(interval), (x, _) => x)
                .Timestamp()
                .Subscribe(x =>
                    {
                        var message = $"buffer {x.Timestamp} - count = {x.Value.Count()}, values - {x.Value.First()}, {x.Value.Last()}";
                        Console.WriteLine(message);
                    });

答案 2 :(得分:0)

这是 PartitionWithInterval 运算符的一个实现,它针对内存效率进行了优化。 IObservable<IEnumerable<T>> 发出的可枚举是惰性枚举的,刚好足以产生下一个或两个分区。然后他们的枚举被暂停,直到下一个时间间隔。为了实现这种惰性,该实现使用 IAsyncEnumerable 代替 IObservable,并使用 System.Linq.AsyncSystem.Interactive.Async 包中的运算符。

public static IObservable<IList<T>> PartitionWithInterval<T>(
    this IObservable<IEnumerable<T>> source, int size,
    TimeSpan interval, IScheduler scheduler = null)
{
    var observable = Observable.Defer(() =>
    {
        Task delayTask = Task.CompletedTask;
        return source
            .ToAsyncEnumerable()
            .SelectMany(x => x.ToAsyncEnumerable()).Buffer(size) /* Behavior A */
            //.SelectMany(x => x.ToAsyncEnumerable().Buffer(size)) /* Behavior B */
            .Do(async (_, cancellationToken) =>
            {
                await delayTask;
                var timer = scheduler != null ?
                    Observable.Timer(interval, scheduler) : Observable.Timer(interval);
                delayTask = timer.ToTask(cancellationToken);
            })
            .ToObservable();
    });
    return scheduler != null ? observable.ObserveOn(scheduler) : observable;
}

下面是一个弹珠图,显示了 PartitionWithInterval 运算符的行为,配置了 size: 2

Source: +----[1,2,3,4,5]--------------------[6,7,8,9]---|
Output: +----[1,2]-------[3,4]--------------[5,6]-------[7,8]-------[9]|

如图所示,一个输出分区可能包含来自多个枚举的值(上图中的分区 [5,6])。如果这是不可取的,只需注释“行为 A”行并取消注释“行为 B”行。下面的大理石图显示了这种变化的效果:

Source: +----[1,2,3,4,5]--------------------[6,7,8,9]---|
Output: +----[1,2]-------[3,4]-------[5]-------[6,7]-------[8,9]|

注意: 对于懒惰地枚举源 observable 发出的可枚举的意图,上述解决方案并不是绝对令人满意的。理想的情况是在每个分区应该发出时准确地生成它。相反,上述实现在发出前一个分区后立即收集下一个分区的元素。另一种方法是在发出每个分区后强制延迟,包括最后一个分区。这会将结果 IObservable 的完成推迟等于 interval 的时间跨度,这也不理想(此行为由本答案的 revision 3 实现)。理想的行为可能可以通过重新实现运算符 ToAsyncEnumerableSelectManyBufferDo 来实现,以便它们传达状态 IsLast当前发射的元素。即使这是可能的,也需要付出很多努力才能实现如此微不足道的改进。

相关问题