我有一个遗留的基于事件的对象,看起来非常适合RX:在连接到网络源之后,它会在收到消息时引发事件,并且可能因单个错误而终止(连接死亡等) 。)或(很少)表示不再有消息。此对象还有一些预测 - 大多数用户只对收到的消息的子集感兴趣,因此只有在众所周知的消息子类型出现时才会引发备用事件。
因此,在学习更多关于反应式编程的过程中,我构建了以下包装器:
class LegacyReactiveWrapper : IConnectableObservable<TopLevelMessage>
{
private LegacyType _Legacy;
private IConnectableObservable<TopLevelMessage> _Impl;
public LegacyReactiveWrapper(LegacyType t)
{
_Legacy = t;
var observable = Observable.Create<TopLevelMessage>((observer) =>
{
LegacyTopLevelMessageHandler tlmHandler = (sender, tlm) => observer.OnNext(tlm);
LegacyErrorHandler errHandler = (sender, err) => observer.OnError(new ApplicationException(err.Message));
LegacyCompleteHandler doneHandler = (sender) => observer.OnCompleted();
_Legacy.TopLevelMessage += tlmHandler;
_Legacy.Error += errHandler;
_Legacy.Complete += doneHandler;
return Disposable.Create(() =>
{
_Legacy.TopLevelMessage -= tlmHandler;
_Legacy.Error -= errHandler;
_Legacy.Complete -= doneHandler;
});
});
_Impl = observable.Publish();
}
public IDisposable Subscribe(IObserver<TopLevelMessage> observer)
{
return _Impl.RefCount().Subscribe(observer);
}
public IDisposable Connect()
{
_Legacy.ConnectToMessageSource();
return Disposable.Create(() => _Legacy.DisconnectFromMessageSource());
}
public IObservable<SubMessageA> MessageA
{
get
{
// This is the moral equivalent of the projection behavior
// that already exists in the legacy type. We don't hook
// the LegacyType.MessageA event directly.
return _Impl.RefCount()
.Where((tlm) => tlm.MessageType == MessageType.MessageA)
.Select((tlm) => tlm.SubMessageA);
}
}
public IObservable<SubMessageB> MessageB
{
get
{
return _Impl.RefCount()
.Where((tlm) => tlm.MessageType == MessageType.MessageB)
.Select((tlm) => tlm.SubMessageB);
}
}
}
关于它如何在其他地方使用的东西感觉......不管怎样......不过。这是示例用法,虽然有效,但感觉很奇怪。我的测试应用程序的UI上下文是WinForms,但它并不重要。
// in Program.Main...
MainForm frm = new MainForm();
// Updates the UI based on a stream of SubMessageA's
IObserver<SubMessageA> uiManager = new MainFormUiManager(frm);
LegacyType lt = new LegacyType();
// ... setup lt...
var w = new LegacyReactiveWrapper(lt);
var uiUpdateSubscription = (from msgA in w.MessageA
where SomeCondition(msgA)
select msgA).ObserveOn(frm).Subscribe(uiManager);
var nonUiSubscription = (from msgB in w.MessageB
where msgB.SubType == MessageBType.SomeSubType
select msgB).Subscribe(
m => Console.WriteLine("Got MsgB: {0}", m),
ex => Console.WriteLine("MsgB error: {0}", ex.Message),
() => Console.WriteLine("MsgB complete")
);
IDisposable unsubscribeAtExit = null;
frm.Load += (sender, e) =>
{
var connectionSubscription = w.Connect();
unsubscribeAtExit = new CompositeDisposable(
uiUpdateSubscription,
nonUiSubscription,
connectionSubscription);
};
frm.FormClosing += (sender, e) =>
{
if(unsubscribeAtExit != null) { unsubscribeAtExit.Dispose(); }
};
Application.Run(frm);
此工作 - 表单启动,UI更新,当我关闭它时,订阅被清理并且进程退出(如果LegacyType的网络连接仍然连接,它将不会执行)。严格来说,仅仅处理connectionSubscription
就足够了。但是,明确的Connect
对我来说很奇怪。由于RefCount
应该为您执行此操作,因此我尝试修改包装器,以便在_Impl.RefCount
和MessageA
中使用MessageB
并在以后显式连接时使用{this.RefCount
1}}而是将对Subscribe
的调用移动到Load
处理程序。这有一个不同的问题 - 第二个订阅触发了对LegacyReactiveWrapper.Connect
的另一个调用。我认为Publish
/ RefCount
背后的想法是“先进先出触发连接,最后一次配置连接。”
我想我有三个问题:
Publish
/ RefCount
? IConnectableObservable<T>
,但不涉及授权通过IObservable<T>.Publish
获得的方式?我知道你不应该自己实现IObservable<T>
,但我不明白如何将连接逻辑注入IConnectableObservable<T>
给你的Observable.Create().Publish()
。 Connect
应该是幂等的吗?答案 0 :(得分:2)
我不确定您是否需要直接公开Connect。我将简化如下,使用Publish().RefCount()
作为封装的实现细节;这将导致仅在需要时进行传统连接。这里第一个用户导致连接,最后一个用户导致断开连接。另请注意,这正确地在所有订阅者之间共享一个RefCount
,而您的实现使用每个消息类型RefCount
,这可能不是预期的。用户无需明确连接:
public class LegacyReactiveWrapper
{
private IObservable<TopLevelMessage> _legacyRx;
public LegacyReactiveWrapper(LegacyType legacy)
{
_legacyRx = WrapLegacy(legacy).Publish().RefCount();
}
private static IObservable<TopLevelMessage> WrapLegacy(LegacyType legacy)
{
return Observable.Create<TopLevelMessage>(observer =>
{
LegacyTopLevelMessageHandler tlmHandler = (sender, tlm) => observer.OnNext(tlm);
LegacyErrorHandler errHandler = (sender, err) => observer.OnError(new ApplicationException(err.Message));
LegacyCompleteHandler doneHandler = sender => observer.OnCompleted();
legacy.TopLevelMessage += tlmHandler;
legacy.Error += errHandler;
legacy.Complete += doneHandler;
legacy.ConnectToMessageSource();
return Disposable.Create(() =>
{
legacy.DisconnectFromMessageSource();
legacy.TopLevelMessage -= tlmHandler;
legacy.Error -= errHandler;
legacy.Complete -= doneHandler;
});
});
}
public IObservable<TopLevelMessage> TopLevelMessage
{
get
{
return _legacyRx;
}
}
public IObservable<SubMessageA> MessageA
{
get
{
return _legacyRx.Where(tlm => tlm.MessageType == MessageType.MessageA)
.Select(tlm => tlm.SubMessageA);
}
}
public IObservable<SubMessageB> MessageB
{
get
{
return _legacyRx.Where(tlm => tlm.MessageType == MessageType.MessageB)
.Select(tlm => tlm.SubMessageB);
}
}
}
另一个观察是Publish().RefCount()
将在订阅者数量达到0时删除基础订阅。通常,当我需要维护订阅时,即使订阅者数量很多,我也只使用Connect
公布的来源降至零(稍后可能会再次恢复)。虽然很少需要这样做 - 只有当你可能不需要连接比保留订阅资源更昂贵时才会这样做。
答案 1 :(得分:1)
您的理解并非完全错误,但您似乎确实存在一些误解。
您似乎认为在同一个源IObservable上多次调用RefCount
将导致共享引用计数。他们不;每个实例都有自己的计数。因此,您正在对_Impl进行多次订阅,每次调用一次订阅或调用Message属性。
您也可能期望_Impl
以及IConnectableObservable
以某种方式调用您的Connect
方法(因为您似乎很惊讶您需要在消费代码中调用Connect)。所有Publish
都会导致发布对象的订阅者(从.Publish()调用返回)共享对底层源可观察对象的单个订阅(在这种情况下,通过调用Observable.Create创建的对象)
通常情况下,我会立即一起使用Publish和RefCount(例如source.Publish().RefCount()
)以获得上述共享订阅效果,或者在不需要调用Connect以开始订阅原始源的情况下进行冷可观察热。但是,这依赖于使用.Publish()。RefCount()为所有订阅者返回的相同对象(如上所述)。
您对Connect的实施似乎是合理的。我不知道有关Connect是否应该是幂等的任何建议,但我个人不会期望它。如果你想要它,你只需要跟踪它的回调处理它的返回值,以获得正确的平衡。
我认为您不需要像现在这样使用Publish,除非有理由避免将多个事件处理程序附加到旧对象。如果您确实需要避免这种情况,我建议您将_Impl
更改为普通IObservable
,并使用Publish
关注RefCount
。
您的MessageA
和MessageB
属性可能会成为用户混淆的根源,因为它们返回IObservable,但仍需要在基础对象上调用Connect才能开始接收消息。我要么将它们更改为IConnectableObservables,它们以某种方式委托给原始的Connect(此时幂等性讨论变得更加相关)或删除属性,让用户在需要时自己制作(相当简单的)投影。