我正在寻找一个Rx方法,该方法将采用一个可观察的并将最新项目置于“冷却时间”,这样当物品的进入速度低于冷却时间时,它们只是被转发但是当它们进入时你可以在每个冷却时间后获得最新值。
换句话说,当项目间隔时间小于t
时,我希望切换到期间为t
的采样(并在它们展开时切换回来)。
这与Observable.Throttle的确非常相似,只是每当新项目到达时都不会重置计时器。
我想到的应用程序是通过网络发送“最新价值”更新。我不想传达一个价值,除非它已经改变,我不想发送快速变化的价值,以至于我淹没了其他数据。
是否有标准方法可以满足我的需求?
答案 0 :(得分:4)
此实现还有另一个有趣的区别 - 它与Sample
方法不同,因为它会立即发出在冷却时间之外而不是在下一个采样间隔发生的事件。它在冷却时间之外没有计时器。
编辑 - 这是v3解决了Chris在评论中提到的问题 - 它确保在冷却过程中发生的变化会触发新的冷却期。
public static IObservable<T> LimitRate<T>(
this IObservable<T> source, TimeSpan duration, IScheduler scheduler)
{
return source.DistinctUntilChanged()
.GroupByUntil(k => 0,
g => Observable.Timer(duration, scheduler))
.SelectMany(x => x.FirstAsync()
.Merge(x.Skip(1)
.TakeLast(1)))
.Select(x => Observable.Return(x)
.Concat(Observable.Empty<T>()
.Delay(duration, scheduler)))
.Concat();
}
这最初使用GroupByUntil
将所有事件打包到冷却期间的同一组中。它会监视更改并在组到期时发出最终更改(如果有)。
然后将生成的事件投影到OnCompleted被冷却期延迟的流中。然后将这些流连接在一起。这可以防止事件比冷却更紧密,但是否则会尽快发出。
以下是使用nuget包rx-testing
和nunit
运行的单元测试(针对v3编辑更新):
public class LimitRateTests : ReactiveTest
{
[Test]
public void SlowerThanRateIsUnchanged()
{
var scheduler = new TestScheduler();
var source = scheduler.CreateColdObservable(
OnNext(200, 1),
OnNext(400, 2),
OnNext(700, 3));
var results = scheduler.CreateObserver<int>();
source.LimitRate(TimeSpan.FromTicks(100), scheduler).Subscribe(results);
scheduler.Start();
results.Messages.AssertEqual(
OnNext(200, 1),
OnNext(400, 2),
OnNext(700, 3));
}
[Test]
public void FasterThanRateIsSampled()
{
var scheduler = new TestScheduler();
var source = scheduler.CreateColdObservable(
OnNext(100, 1),
OnNext(140, 5),
OnNext(150, 2),
OnNext(300, 3),
OnNext(350, 4));
var results = scheduler.CreateObserver<int>();
source.LimitRate(TimeSpan.FromTicks(100), scheduler).Subscribe(results);
scheduler.Start();
results.Messages.AssertEqual(
OnNext(100, 1),
OnNext(200, 2),
OnNext(300, 3),
OnNext(400, 4));
}
[Test]
public void DuplicatesAreOmitted()
{
var scheduler = new TestScheduler();
var source = scheduler.CreateColdObservable(
OnNext(100, 1),
OnNext(150, 1),
OnNext(300, 1),
OnNext(350, 1));
var results = scheduler.CreateObserver<int>();
source.LimitRate(TimeSpan.FromTicks(100), scheduler).Subscribe(results);
scheduler.Start();
results.Messages.AssertEqual(
OnNext(100, 1));
}
[Test]
public void CoolResetsCorrectly()
{
var scheduler = new TestScheduler();
var source = scheduler.CreateColdObservable(
OnNext(100, 1),
OnNext(150, 2),
OnNext(205, 3));
var results = scheduler.CreateObserver<int>();
source.LimitRate(TimeSpan.FromTicks(100), scheduler).Subscribe(results);
scheduler.Start();
results.Messages.AssertEqual(
OnNext(100, 1),
OnNext(200, 2),
OnNext(300, 3));
}
[Test]
public void MixedPacingWorks()
{
var scheduler = new TestScheduler();
var source = scheduler.CreateColdObservable(
OnNext(100, 1),
OnNext(150, 1),
OnNext(450, 3),
OnNext(750, 4),
OnNext(825, 5));
var results = scheduler.CreateObserver<int>();
source.LimitRate(TimeSpan.FromTicks(100), scheduler).Subscribe(results);
scheduler.Start();
results.Messages.AssertEqual(
OnNext(100, 1),
OnNext(450, 3),
OnNext(750, 4),
OnNext(850, 5));
}
}
答案 1 :(得分:2)
您可以使用Observable.DistinctUntilChanged
和Observable.Sample
。
<强> Observable.DistinctUntilChanged 强>
此方法仅在与前一个值不同时才会显示值。 (http://www.introtorx.com/content/v1.0.10621.0/05_Filtering.html)
<强> Observable.Sample 强>
Sample方法只为每个指定的TimeSpan取最后一个值。 (http://www.introtorx.com/content/v1.0.10621.0/13_TimeShiftedSequences.html#Sample)
要生成所需效果,您可以将生成的第一个项目与上述项目结合起来。
答案 2 :(得分:2)
我意识到这已经有一段时间了,但是我想提供一种替代解决方案,我认为更准确地符合原始要求。该解决方案引入了2个自定义运算符。
首先是SampleImmediate
,其工作原理与Sample
完全相同,但它会立即发送第一个项目。这是通过许多操作员完成的。 Materialize
/ Dematerialize
和DistinctUntilChanged
一起工作以确保不会发送重复的通知。 Merge
,Take(1)
和Sample
提供基本的“立即采样”功能。 Publish
和Connect
将这些联系起来。 GroupBy
和SelectMany
确保我们在启动计时器之前等待第一个事件产生。 Create
帮助我们妥善处理所有事情。
public static IObservable<T> SampleImmediate<T>(this IObservable<T> source, TimeSpan dueTime)
{
return source
.GroupBy(x => 0)
.SelectMany(group =>
{
return Observable.Create<T>(o =>
{
var connectable = group.Materialize().Publish();
var sub = Observable.Merge(
connectable.Sample(dueTime),
connectable.Take(1)
)
.DistinctUntilChanged()
.Dematerialize()
.Subscribe(o);
return new CompositeDisposable(connectable.Connect(), sub);
});
});
}
在我们SampleImmediate
之后,我们可以使用Cooldown
创建GroupByUntil
,以便在我们的滑动Throttle
窗口关闭之前对发生的所有事件进行分组。一旦我们有了我们的小组,我们只需SampleImmediate
整个小组。
public static IObservable<T> Cooldown<T>(this IObservable<T> source, TimeSpan dueTime)
{
return source
.GroupByUntil(x => 0, group => group.Throttle(dueTime))
.SelectMany(group => group.SampleImmediate(dueTime));
}
我不建议这个解决方案是更好或更快,我只是觉得看到另一种方法可能会很好。
答案 3 :(得分:0)
自我回答。
虽然我问Rx,但我的实际情况是它的端口(ReactiveCocoa)。更多的人都知道Rx,我可以翻译。
无论如何,我最终直接实现了它,以便它能够满足我想要的延迟/性能属性:
-(RACSignal*)cooldown:(NSTimeInterval)cooldownPeriod onScheduler:(RACScheduler *)scheduler {
need(cooldownPeriod >= 0);
need(!isnan(cooldownPeriod));
need(scheduler != nil);
need(scheduler != RACScheduler.immediateScheduler);
force(cooldownPeriod != 0); //todo: bother with no-cooldown case?
force(!isinf(cooldownPeriod)); //todo: bother with infinite case?
return [[RACSignal createSignal:^(id<RACSubscriber> subscriber) {
need(subscriber != nil);
NSObject* lock = [NSObject new];
__block bool isCoolingDown = false;
__block bool hasDelayedValue = false;
__block id delayedValue = nil;
__block RACDisposable *cooldownDisposer = nil;
void (^onCanSendValue)(void) = ^{
@synchronized (lock) {
// check that we were actually cooling down
// (e.g. what if the system thrashed before we could dispose the running-down timer, causing a redundant call?)
if (!isCoolingDown) {
return;
}
// if no values arrived during the cooldown, we do nothing and can stop the timer for now
if (!hasDelayedValue) {
isCoolingDown = false;
[cooldownDisposer dispose];
return;
}
// forward latest value
id valueToSend = delayedValue;
hasDelayedValue = false;
delayedValue = nil;
// todo: can this be avoided?
// holding a lock while triggering arbitrary actions cam introduce subtle deadlock cases...
[subscriber sendNext:valueToSend];
}
};
void (^preemptivelyEndCooldown)(void) = ^{
// forward latest value AND ALSO force cooldown to run out (disposing timer)
onCanSendValue();
onCanSendValue();
};
RACDisposable *selfDisposable = [self subscribeNext:^(id x) {
bool didStartCooldown;
@synchronized (lock) {
hasDelayedValue = true;
delayedValue = x;
didStartCooldown = !isCoolingDown;
isCoolingDown = true;
}
if (didStartCooldown) {
// first item gets sent right away
onCanSendValue();
// coming items have to wait for the timer to run down
cooldownDisposer = [[RACSignal interval:cooldownPeriod onScheduler:scheduler]
subscribeNext:^(id _) { onCanSendValue(); }];
}
} error:^(NSError *error) {
preemptivelyEndCooldown();
[subscriber sendError:error];
} completed:^{
preemptivelyEndCooldown();
[subscriber sendCompleted];
}];
return [RACDisposable disposableWithBlock:^{
[selfDisposable dispose];
@synchronized (lock) {
isCoolingDown = false;
[cooldownDisposer dispose];
}
}];
}] setNameWithFormat:@"[%@ cooldown:%@]", self.name, @(cooldownPeriod)];
}
它应该几乎直接转换为.Net RX。当物品停止到达时,它将停止做任何工作,并且会在尊重冷却时尽快转发物品。