我有以下Rx扩展方法来分区IEnumerable< T>并延迟每个分区值的产生。它使用IEnumerable< T>分区数据的扩展,也用单元测试显示。
是否有更好的方法来延迟'而不是使用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);
}
}
答案 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
实际调用Partition
和Interval
。
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.Async 和 System.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 实现)。理想的行为可能可以通过重新实现运算符 ToAsyncEnumerable
、SelectMany
、Buffer
和 Do
来实现,以便它们传达状态 IsLast
当前发射的元素。即使这是可能的,也需要付出很多努力才能实现如此微不足道的改进。