如何在没有任何副作用的情况下使用 Reactive Extensions (C#) 控制计时器?

How to control a timer using Reactive Extensions (C#) without any side-effects?

我正在尝试实现以下场景:

  1. 一个按钮被按下。
  2. 通过调用GetInterval()方法计算出一个interval
  3. 立即调用DoSomething()方法。
  4. DoSomething() 方法将每隔 interval 毫秒重复调用一次,直到释放按钮。
  5. 再次按下按钮时,转到2。

可以这样实现:

Timer timer = new Timer();
timer.Tick += DoSomething();

//keyDown and keyUp are IObservables created from the button's events
keyDown.Do(_ =>
{
    timer.Interval = GetInterval();
    timer.Start();
    DoSomething();                
});

keyUp.Do(_ =>
{
    timer.Stop();
});

我知道这段代码有一些问题,但没关系。我想要实现的是使用纯 Reactive Extensions 实现这个场景,没有任何外部计时器和副作用(除了调用 DoSomething() 方法)。

我知道 Observable.Timer,但我不知道如何在这种情况下使用它。

编辑: 我可以创建一个包含 interval 当前值的 IObservable<int>。它可能会有所帮助。

keyDown
   .SelectMany(_ => 
       Observable.Interval(TimeSpan.FromMilliseconds(500))
          .TakeUntil(keyUp))
   .Subscribe(_ => DoSomething());

这将在每个按键事件上启动一个间隔。计时器将在发出 keyup 事件时完成。

我建议使用 Select/Switch 的方法比使用 SelectMany 的方法更好。

一般来说,当使用 SelectMany 时,随着越来越多的基础订阅被创建,您最终可能会出现内存泄漏。由于 TakeUntil,在这种情况下可能不会发生这种情况,但值得养成避免这种情况的习惯。

这是我的代码:

var subscription =
    keyDown
        .Select(kd =>
            Observable
                .Interval(TimeSpan.FromMilliseconds(500))
                .TakeUntil(keyUp)
                .StartWith(-1L))
        .Switch()
        .Subscribe(n => DoSomething());

当使用 Switch 时,您必须构造一个 IObservable<IObservable<T>> 来调用 .Switch(),它采用 IObservable<IObservable<T>> 生成的最新 IObservable<T> 并将其展平与 SelectMany 类似,但前者处理了先前的可观察对象,而后者则没有。

我还包括了一个 StartWith 确保序列立即产生一个值。