Получить предыдущий элемент в IObservable без повторной оценки последовательности

В последовательности IObservable (в Reactive Extensions для .NET) я хотел бы получить значение предыдущего и текущего элементов, чтобы я мог их сравнить. Я нашел пример онлайн, подобный ниже, который выполняет задачу:

sequence.Zip(sequence.Skip(1), (prev, cur) => new { Previous = prev, Current = cur })

Он отлично работает, за исключением того, что он дважды оценивает последовательность, чего я бы хотел избежать. Вы можете видеть, что он дважды оценивается с помощью этого кода:

var debugSequence = sequence.Do(item => Debug.WriteLine("Retrieved an element from sequence"));
debugSequence.Zip(debugSequence.Skip(1), (prev, cur) => new { Previous = prev, Current = cur }).Subscribe();

Вывод показывает в два раза больше строк отладки, так как в последовательности есть элементы.

Я понимаю, почему это происходит, но до сих пор я не нашел альтернативы, которая не оценивает последовательность дважды. Как я могу объединить предыдущие и текущие только с одной оценкой последовательности?

Ответ 1

Там лучшее решение для этого, я думаю, использует Observable.Scan и избегает двойной подписки:

public static IObservable<Tuple<TSource, TSource>>
    PairWithPrevious<TSource>(this IObservable<TSource> source)
{
    return source.Scan(
        Tuple.Create(default(TSource), default(TSource)),
        (acc, current) => Tuple.Create(acc.Item2, current));
}

Я написал это в своем блоге здесь: http://www.zerobugbuild.com/?p=213

Добавление

Дополнительная модификация позволяет более эффективно работать с произвольными типами с помощью селектора результатов:

public static IObservable<TResult> CombineWithPrevious<TSource,TResult>(
    this IObservable<TSource> source,
    Func<TSource, TSource, TResult> resultSelector)
{
    return source.Scan(
        Tuple.Create(default(TSource), default(TSource)),
        (previous, current) => Tuple.Create(previous.Item2, current))
        .Select(t => resultSelector(t.Item1, t.Item2));
}

Ответ 2

Оценка дважды - индикатор Холодного наблюдаемого. Вы можете включить его в Hot, используя .Publish():

var pub = sequence.Publish();
pub.Zip(pub.Skip(1), (...
pub.Connect();

Ответ 3

@James World addendum выглядит здорово для меня, если не для Tuple<>, что мне почти всегда не нравится: "Был ли .Item1 предыдущим? Или это был текущий? Я не помню. И какой первый аргумент к селектору, это был предыдущий элемент?".

Для этой части мне понравилось определение @dcstraw выделенного ItemWithPrevious<T>. Итак, вы идете, поставив их вместе (надеюсь, я не смешивал предыдущие с текущими) с некоторыми переименованиями и возможностями:

public static class ObservableExtensions
{
    public static IObservable<SortedPair<TSource>> CombineWithPrevious<TSource>(
        this IObservable<TSource> source, 
        TSource initialValue = default(TSource))
    {
        var seed = SortedPair.Create(initialValue, initialValue);

        return source.Scan(seed,
            (acc, current) => SortedPair.Create(current, acc.Current));
    }

    public static IObservable<TResult> CombineWithPrevious<TSource, TResult>(
        this IObservable<TSource> source,
        Func<SortedPair<TSource>, TResult> resultSelector,
        TSource initialValue = default(TSource))
    {
        var seed = SortedPair.Create(initialValue, initialValue);

        return source
            .Scan(seed,
                (acc, current) => SortedPair.Create(current, acc.Current))
            .Select(p => resultSelector(p));
    }
}

public class SortedPair<T>
{
    public SortedPair(T current, T previous)
    {
        Current = current;
        Previous = previous;
    }

    public SortedPair(T current) : this(current, default(T)) { }

    public SortedPair() : this(default(T), default(T)) { }

    public T Current;
    public T Previous;
}

public class SortedPair
{
    public static SortedPair<T> Create<T>(T current, T previous)
    {
        return new SortedPair<T>(current, previous);
    }

    public static SortedPair<T> Create<T>(T current)
    {
        return new SortedPair<T>(current);
    }

    public static SortedPair<T> Create<T>()
    {
        return new SortedPair<T>();
    }
}

Ответ 4

Если вам нужно только получить доступ к предыдущему элементу во время подписки, это, вероятно, самая простая вещь, которая будет работать. (Я уверен, что есть лучший способ, возможно, оператор буфера на IObservable? Документация на данный момент довольно разрежена, поэтому я не могу сказать вам.)

    EventArgs prev = null;

    sequence.Subscribe(curr => 
    {
        if (prev != null)
        {
            // Previous and current element available here
        }

        prev = curr;                              

    });

EventArgs - это всего лишь резерв для типа аргумента вашего события.

Ответ 6

Оказывается, вы можете использовать переменную для хранения предыдущего значения и ссылаться на нее и переназначить ее в цепочке расширений IObservable. Это даже работает в вспомогательном методе. С приведенным ниже кодом я могу теперь вызвать CombineWithPrevious() на моем IObservable, чтобы получить ссылку на предыдущее значение, без повторной оценки последовательности.

public class ItemWithPrevious<T>
{
    public T Previous;
    public T Current;
}

public static class MyExtensions
{
    public static IObservable<ItemWithPrevious<T>> CombineWithPrevious<T>(this IObservable<T> source)
    {
        var previous = default(T);

        return source
            .Select(t => new ItemWithPrevious<T> { Previous = previous, Current = t })
            .Do(items => previous = items.Current);
    }
}