我需要一个RX操作,该操作会反跳流中的元素,直到值更改为止。如果该值在一段时间内没有变化,它还必须支持超时以发出最后一个元素。
t标记超时
DistinctUntilChanged
有点类似,但是我想要最后一个相等的项目,而不是第一个。我尝试使用Buffer
和GroupBy
并选择了组中的最后一个元素,但是我需要使用计时器重置每个元素,以确保在选择最后一个元素之前该组包含所有相等的元素。
我制作了一个使用Timeout
和Retry
的实现,但是我对每次超时都必须重新订阅源并不满意,因为这可能并不适合所有情况/源(即冷观测值)。似乎可以与我测试过的热门可观察物一起正常工作。
public static IObservable<TSource> ThrottleBy<TSource, TKey>(this IObservable<TSource> source, Func<TSource, TKey> keySelector, TimeSpan timeout, IEqualityComparer<TKey> comparer = null, IScheduler scheduler = null)
{
comparer ??= EqualityComparer<TKey>.Default;
scheduler ??= DefaultScheduler.Instance;
var prev = default(TSource);
return source
.StartWith(default(TSource))
.Select(e =>
{
var ret = !comparer.Equals(keySelector(prev), keySelector(e)) ? prev : default;
prev = e;
return ret;
})
.Where(e => !Equals(e, default(TSource)))
.Timeout(timeout, scheduler)
.RetryWhen(ex => ex.OfType<TimeoutException>());
}
由于Throttle在Rx.NET中的工作方式,因此称其为ThrottleBy而不是DebounceBy。
关于如何/应该实施这种操作的任何想法?
答案 0 :(得分:0)
编辑:感谢您使用大理石图。我把它变成了一些测试用例。您是正确的,我以前的解决方案缺少计时器。我在这里补充说,这是将每条消息加倍的形式,但其中一条会立即通过,另一条会延迟。解决方法如下:
public static IObservable<TSource> ThrottleBy4<TSource, TKey>(this IObservable<TSource> source, Func<TSource, TKey> keySelector, TimeSpan timeout,
IEqualityComparer<TKey> comparer = null, IScheduler scheduler = null)
{
comparer = comparer ?? EqualityComparer<TKey>.Default;
scheduler = scheduler ?? DefaultScheduler.Instance;
return source
.Timestamp(scheduler)
.Publish(_val => Observable.Merge( // For every incoming item, create two items: One immediate, one delayed by the timeout time.
_val.Select(v => (value: v, isOriginal: true)),
_val.Select(v => (value: v, isOriginal: false)).Delay(timeout, scheduler)
))
.StateSelect(Timestamped.Create(default(TSource), DateTimeOffset.MinValue),
(prevVal, t) => // Result function
{
// special handling for the initial state
if (prevVal.Timestamp == DateTimeOffset.MinValue)
return (prevVal, false);
if (t.isOriginal) // If an original value, only emit if the value changed.
return (prevVal, !comparer.Equals(keySelector(t.value.Value), keySelector(prevVal.Value)));
else // If a repeat value, only emit if the prevVal state is the same timestamp and value.
return (prevVal, comparer.Equals(keySelector(t.value.Value), keySelector(prevVal.Value)) && t.value.Timestamp == prevVal.Timestamp);
},
(prevVal, t) => t.isOriginal ? t.value : prevVal // State function. Only change state if the incoming item is an original value.
)
.Where(t => t.Item2)
.Select(t => t.Item1.Value);
}
这是测试代码:
TestScheduler ts = new TestScheduler();
var source = ts.CreateHotObservable<string>(
new Recorded<Notification<string>>(200.MsTicks(), Notification.CreateOnNext("A1")),
new Recorded<Notification<string>>(300.MsTicks(), Notification.CreateOnNext("A2")),
new Recorded<Notification<string>>(500.MsTicks(), Notification.CreateOnNext("B1")),
new Recorded<Notification<string>>(800.MsTicks(), Notification.CreateOnNext("B2"))
);
var comparer = new FirstLetterComparer();
var target = source
.ThrottleBy4(s => s, TimeSpan.FromSeconds(1), comparer: comparer, scheduler: ts);
var expectedResults = ts.CreateHotObservable<string>(
new Recorded<Notification<string>>(500.MsTicks(), Notification.CreateOnNext("A2")),
new Recorded<Notification<string>>(1800.MsTicks(), Notification.CreateOnNext("B2"))
);
var observer = ts.CreateObserver<string>();
target.Subscribe(observer);
ts.Start();
ReactiveAssert.AreElementsEqual(expectedResults.Messages, observer.Messages);
以及这些帮助程序类:
public class FirstLetterComparer : IEqualityComparer<string>
{
public bool Equals(string s1, string s2)
{
if (s1 == null && s2 == null)
return true;
if (s1 == null || s2 == null)
return false;
return (s1[0] == s2[0]);
}
public int GetHashCode(string s)
{
return s == null ? 0 : s[0].GetHashCode();
}
}
public static class X
{
public static long MsTicks(this int i)
{
return TimeSpan.FromMilliseconds(i).Ticks;
}
}
上一个答案:
除了Timeout
问题之外,我发现您的解决方案还有两个可能的问题:
default(T)
作为令牌值会在某个时候使您绊倒。例如,这将不允许0
进入IObservable<int>
。prev
,因此您可能会遇到多重订阅问题。多个订阅者会共享该字段,这可能导致竞争状况和错误行为。您可以通过返回一个元组来解决这两个问题,其中一个值带有布尔值(是否为新),另一个值带有值:
public static IObservable<TSource> ThrottleBy2<TSource, TKey>(this IObservable<TSource> source, Func<TSource, TKey> keySelector, TimeSpan timeout,
IEqualityComparer<TKey> comparer = null, IScheduler scheduler = null)
{
comparer = comparer ?? EqualityComparer<TKey>.Default;
scheduler = scheduler ?? DefaultScheduler.Instance;
return source
.StateSelect(default(TSource), (prevVal, newVal) => (!comparer.Equals(keySelector(prevVal), keySelector(newVal)), newVal), (_, newVal) => newVal)
.Where(t => t.Item1)
.Select(t => t.newVal)
.Timeout(timeout, scheduler)
.RetryWhen(ex => ex.OfType<TimeoutException>());
}
StateSelect
在这里执行您想要的操作:它维护一个状态(您以前在prev
字段中拥有的状态),并返回前面提到的元组。看起来像这样:
public static IObservable<TResult> StateSelect<TSource, TState, TResult>(this IObservable<TSource> source, TState initialState,
Func<TState, TSource, TResult> resultSelector, Func<TState, TSource, TState> stateSelector)
{
return source
.StateSelectMany(initialState, (state, item) => Observable.Return(resultSelector(state, item)), stateSelector);
}
public static IObservable<TResult> StateSelectMany<TSource, TState, TResult>(this IObservable<TSource> source, TState initialState,
Func<TState, TSource, IObservable<TResult>> resultSelector, Func<TState, TSource, TState> stateSelector)
{
return source
.Scan(Tuple.Create(initialState, Observable.Empty<TResult>()), (state, item) => Tuple.Create(stateSelector(state.Item1, item), resultSelector(state.Item1, item)))
.SelectMany(t => t.Item2);
}
这仍然留下两个小问题:
Timeout
问题default(TSource)
,则将default(TSource)
用作初始状态会导致问题。我们可以通过引入时间戳来解决这两个问题:
public static IObservable<TSource> ThrottleBy3<TSource, TKey>(this IObservable<TSource> source, Func<TSource, TKey> keySelector, TimeSpan timeout,
IEqualityComparer<TKey> comparer = null, IScheduler scheduler = null)
{
comparer = comparer ?? EqualityComparer<TKey>.Default;
scheduler = scheduler ?? DefaultScheduler.Instance;
return source
.Timestamp(scheduler)
.StateSelect(Timestamped.Create(default(TSource), DateTimeOffset.MinValue),
(prevVal, newVal) => (!comparer.Equals(keySelector(prevVal.Value), keySelector(newVal.Value)) || newVal.Timestamp - prevVal.Timestamp > timeout, newVal),
(prevVal, newVal) => !comparer.Equals(keySelector(prevVal.Value), keySelector(newVal.Value)) || newVal.Timestamp - prevVal.Timestamp > timeout ? newVal : prevVal
)
.Where(t => t.Item1)
.Select(t => t.newVal.Value);
}
在这里,我们将带有时间戳的值存储为状态,如果去抖时间足够长或值发生更改,我们将更改状态。结果再次是一个元组,指示该值是否应该继续,以及带有时间戳的值。
希望这会有所帮助。
答案 1 :(得分:0)
多亏了Shlomo的大力帮助,我认为我现在对这个问题有很好的解决方案:
public static IObservable<TSource> ThrottleBy<TSource, TKey>(this IObservable<TSource> source, Func<TSource, TKey> keySelector, TimeSpan timeout,
IEqualityComparer<TKey> comparer = null, IScheduler scheduler = null)
{
comparer = comparer ?? EqualityComparer<TKey>.Default;
scheduler = scheduler ?? DefaultScheduler.Instance;
return source
.Publish(_val => Observable.Merge(
_val.Select(v => (value: v, timeout: false)),
_val.Select(v => (value: v, timeout: true)).Throttle(timeout, scheduler)
))
.Scan((prev: (value: (object)null, timeout: false), emit: (object)null), (state, t) => {
if (state.prev.value == null) // Initial state
return (t, null); // Save new state and ignore
// Emit previous in case of timeout or value changed
if (t.timeout || (!state.prev.timeout && !comparer.Equals(keySelector(t.value), keySelector((TSource)state.prev.value))))
return (t, state.prev.value);
// Save new state and ignore
return (t, null);
})
.Where(x => x.emit != null)
.Select(x => (TSource)x.emit);
}
这很像Shlomo的建议,但是我最终使用了Throttle
而不是Delay
,这使事情变得更加简单。合并辅助方法以使其独立。我将值装箱以避免default(TSource)
。