Наблюдаемый для обратного вызова в Rx

Я ищу элегантный способ создать Observable из простого делегата callback с Rx, что-то похожее на Observable.FromEventPattern?

Скажем, я обертываю Win32 EnumWindows API, который вызывает EnumWindowsProc, который я предоставляю.

Я знаю, что я мог бы создать временный адаптер событий С# для этого обратного вызова и передать его FromEventPattern. Кроме того, я мог бы реализовать IObservable вручную, поэтому он вызывал IObserver.OnNext из моего обратного вызова EnumWindowsProc.

Есть ли существующий шаблон для обертывания обратного вызова в Rx, который мне не хватает?

Ответ 1

Вы можете использовать Subject<T>, который можно использовать для перехода из мира императивного программирования в функциональный мир Rx.

Subject<T> реализует как IObservable<T>, так и IObserver<T>, поэтому вы можете вызвать его методы OnNext, OnError и OnCompleted, и подписчики будут уведомлены.

Если вы хотите показать Subject<T> как свойство, вы должны сделать это, используя .AsObservable(), поскольку это скрывает тот факт, что IObservable<T> на самом деле является Subject<T>. Это делает невозможным такие вещи, как ((Subject<string>) obj.Event).OnNext("Foo").

Ответ 2

Имейте в виду, что обратные вызовы, подобные тем, которые используются в EnumWindows, отличаются от Rx. В частности, обратный вызов может вернуть обратно вызывающему через его возвращаемое значение. Наблюдатели Rx не могут этого сделать. Кроме того, обратные вызовы могут принимать несколько параметров, но наблюдатели Rx получают одно значение. Поэтому вам нужно обернуть несколько параметров в один объект.

Учитывая это, альтернативой использованию Subject является использование Observable.Create. Таким образом, вы регистрируете обратный вызов только тогда, когда на самом деле есть наблюдатель, и вы отменяете его регистрацию, если этот наблюдатель не подписывается.

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

public static IObservable<Foo> WrapFooApi(string arg1, string arg2)
{
    return Observable.Create<Foo>(observer =>
    {
        FooApi.enumerate(arg1, arg2, e =>
        {
            observer.OnNext(new Foo(e));
            return true;
        });

        // In your case, FooApi.enumerate is actually synchronous
        // so when we get to this line of code, we know
        // the stream is complete.
        observer.OnCompleted();
        return Disposable.Empty;
    });
}

// Usage
WrapFooApi("a", "b").Take(1).Subscribe(...); // only takes first item

Мы можем решить проблему, не будучи в состоянии остановить рано, введя небольшую асинхронность, которая даст наблюдателю время, чтобы получить одноразовое, которое он может распоряжаться, чтобы уведомить вас. Мы можем использовать CreateAsync, чтобы получить CancellationToken, который будет отменен, когда наблюдатель отменит подписку. И мы можем запустить код FooApi внутри Task.Run:

public static IObservable<Foo> WrapFooApi(string arg1, string arg2)
{
    return Observable.CreateAsync<Foo>(async (observer, ct) =>
    {
        await Task.Run(() => FooApi.register_callback(arg1, arg2, e =>
        {
            observer.OnNext(e);

            // Returning false will stop the enumeration
            return !ct.IsCancellationRequested;
        }));
        observer.OnCompleted();
    });
}

В более традиционном асинхронном API обратного вызова, где вы регистрируетесь в какой-то момент и отмените регистрацию в какой-то другой точке, вы можете иметь что-то большее:

public static IObservable<Foo> WrapFooApi(string args)
{
    return Observable.Create<Foo>(observer =>
    {
        FooToken token = default(FooToken);
        var unsubscribe = Disposable.Create(() => FooApi.Unregister(token));
        token = FooApi.Register(args, e =>
        {
            observer.OnNext(new Foo(e));
        });

        return unsubscribe;
    });
}