如何在不使用Subject 支持字段的情况下公开IObservable 属性

在这个关于Subject Enigmativity的问题的答案中提到:

另外,你应该尽量避免使用主题。 一般规则是,如果你正在使用一个主题,那么你做错了什么。

我经常使用subject作为IObservable属性的支持字段,这可能是Rx之前几天的.NET事件。 例如,而不是像

 public class Thing { public event EventHandler SomethingHappened; private void DoSomething() { Blah(); SomethingHappened(this, EventArgs.Empty); } } 

我可能会这样做

 public class Thing { private readonly Subject somethingHappened = new Subject(); public IObservable SomethingHappened { get { return somethingHappened; } } private void DoSomething() { Blah(); somethingHappened.OnNext(Unit.Default); } } 

所以,如果我想避免使用Subject ,那么做这种事情的正确方法是什么? 或者我应该坚持在我的界面中使用.NET事件,即使它们被Rx代码使用(可能是FromEventPattern )?

另外,为什么使用这样的Subject是一个坏主意的更多细节将是有帮助的。

更新 :为了使这个问题更具体一点,我所说的是使用Subject作为一种从非Rx代码(可能是你正在使用其他遗留代码)到Rx世界的方法。 所以,像:

 class MyVolumeCallback : LegacyApiForSomeHardware { private readonly Subject volumeChanged = new Subject(); public IObservable VolumeChanged { get { return volumeChanged.AsObservable(); } } protected override void UserChangedVolume(int newVolume) { volumeChanged.OnNext(newVolume); } } 

LegacyApiForSomeHardware类型不是使用事件,而是使用覆盖虚拟方法作为获取“刚刚发生”通知的方式。

首先,有人可以将SomethingHappened转回ISubject并从外部向其中提供东西。 至少,将AsObservable应用于它以隐藏底层对象的主体。

此外,回调的主题广播并不比.NET事件更严格。 例如,如果一个观察者抛出,则不会调用链中下一个观察者。

  static void D() { Action a = null; a += x => { Console.WriteLine("1> " + x); }; a += x => { Console.WriteLine("2> " + x); if (x == 42) throw new Exception(); }; a += x => { Console.WriteLine("3> " + x); }; a(41); try { a(42); // 2> throwing will prevent 3> from observing 42 } catch { } a(43); } static void S() { Subject s = new Subject(); s.Subscribe(x => { Console.WriteLine("1> " + x); }); s.Subscribe(x => { Console.WriteLine("2> " + x); if (x == 42) throw new Exception(); }); s.Subscribe(x => { Console.WriteLine("3> " + x); }); s.OnNext(41); try { s.OnNext(42); // 2> throwing will prevent 3> from observing 42 } catch { } s.OnNext(43); } 

通常,一旦观察者抛出,调用者就会死亡,除非你保护每个On *调用(但不要随意吞下exception,如上所示)。 多播代理也是如此; 例外情况会向你回转。

大多数情况下,您可以在没有主题的情况下实现您想要做的事情,例如使用Observable.Create来构建新序列。 这样的序列没有由多个订阅产生的“观察者列表”; 每个观察者都有自己的“会话”(冷可观察模型),因此观察者的例外仅仅是在狭窄区域内的自杀命令而不是在广场中间吹嘘自己。

本质上,主题最好用在反应式查询图的边缘(对于需要由数据中的另一方提供的入口流,尽管您可以使用常规.NET事件并使用FromEvent *将它们桥接到Rx方法)和用于在被动查询图中共享订阅(使用发布,重放等,这是伪装的多播调用,使用主题)。 使用主题的危险之一 – 由于其观察者列表和潜在的消息记录而非常有状态 – 是在尝试使用主题编写查询运算符时使用它们。 99.999%的时间,这样的故事有一个悲伤的结局。

在Rx论坛上的回答中 ,Dave Sexton( Rxx )说,作为答案的一部分:

主题是Rx的有状态组件。 当您需要创建类似事件的observable作为字段或局部变量时,它们非常有用。

这正是这个问题正在发生的事情,他还撰写了一篇关于使用主题或不使用主题的深入跟进博客文章? 最后得出:

我什么时候应该使用主题?

如果满足以下所有条件:

  • 你没有可观察的或任何可以转换为一个的东西。
  • 你需要一个热的观察。
  • 您的observable的范围是一种类型。
  • 您不需要定义类似的事件,也不存在类似的事件。

在这种情况下,我为什么要使用主题?

因为你别无选择!

因此,回答“为什么使用像这样的主题是一个坏主意的细节”的内在问题 – 这不是一个坏主意,这是使用主题的少数几个地方之一是正确的做事方式。

虽然我不能直接说出Enigmativity,但我想这是因为它非常低级,你不需要直接使用; Subject类提供的所有内容都可以通过使用System.Reactive.Linq命名空间中的类来实现。

Subject文档为例:

 Subject mySubject = new Subject(); //*** Create news feed #1 and subscribe mySubject to it ***// NewsHeadlineFeed NewsFeed1 = new NewsHeadlineFeed("Headline News Feed #1"); NewsFeed1.HeadlineFeed.Subscribe(mySubject); //*** Create news feed #2 and subscribe mySubject to it ***// NewsHeadlineFeed NewsFeed2 = new NewsHeadlineFeed("Headline News Feed #2"); NewsFeed2.HeadlineFeed.Subscribe(mySubject); 

使用Observable类上的Merge扩展方法可以轻松实现这一点:

 IObservable feeds = new NewsHeadlineFeed("Headline News Feed #1").HeadlineFeed.Merge( new NewsHeadlineFeed("Headline News Feed #2").HeadlineFeed); 

然后您可以正常订阅。 使用Subject只会使代码更复杂。 如果您打算使用Subject那么您应该对扩展方法失败的可观察对象进行一些非常低级的处理。

具有简单一次性事件的类的一种方法是提供ToObservable方法,该方法基于事件创建有意义的冷可观察对象。 这比使用Observable工厂方法更具可读性,并允许不使用Rx的开发人员使用API​​。

  public IObservable ToObservable() { return Observable.Create(observer => { Action notifier = () => { switch (Status) { case FutureStatus.Completed: observer.OnNext(Value); observer.OnCompleted(); break; case FutureStatus.Cancelled: observer.OnCompleted(); break; case FutureStatus.Faulted: observer.OnError(Exception); break; } }; Resolve += notifier; return () => Resolve -= notifier; }); }