如何在不使用Subject<T>后备字段的情况下公开IObservable<T>属性

27

在一个有关Subject<T>的问题中,Enigmativity在这个回答中提到:

顺便说一句,你应该尽量避免使用subjects。通常情况下,如果你使用了subject,那么你可能做错了些什么。

我经常使用subjects作为IObservable属性的备用字段,在Rx出现之前它们可能会是.NET事件。例如,代替这样的东西

public class Thing
{
    public event EventHandler SomethingHappened;

    private void DoSomething()
    {
        Blah();
        SomethingHappened(this, EventArgs.Empty);
    }
}

我可能会这样做

public class Thing
{
    private readonly Subject<Unit> somethingHappened = new Subject<Unit>();
    public IObservable<Unit> SomethingHappened
    {
        get { return somethingHappened; }
    }

    private void DoSomething()
    {
        Blah();
        somethingHappened.OnNext(Unit.Default);
    }
}

那么,如果我想避免使用 Subject,应该怎样正确地完成这种操作?或者,我应该坚持在我的接口中使用.NET事件,即使它们将被Rx代码(因此可能是FromEventPattern)使用吗?

另外,更详细地解释为什么像这样使用 Subject 是个坏主意会有所帮助。

更新:为了让这个问题更具体,我在谈论使用 Subject<T> 作为一种从非Rx代码(也许你正在使用其他遗留代码)进入Rx世界的方式。 所以,类似于:

class MyVolumeCallback : LegacyApiForSomeHardware
{
    private readonly Subject<int> volumeChanged = new Subject<int>();

    public IObservable<int> VolumeChanged
    {
        get
        {
            return volumeChanged.AsObservable();
        }
    }

    protected override void UserChangedVolume(int newVolume)
    {
        volumeChanged.OnNext(newVolume);
    }
}

LegacyApiForSomeHardware是一种类型,它让你通过重写虚方法来获取“刚刚发生”的通知,而不是使用事件。

4个回答

17

首先,有人可以将SomethingHappened转换回ISubject并从外部向其中传递内容。至少,应该对其应用AsObservable来隐藏底层对象的主题特性。

另外,主题广播回调并不比.NET事件功能更强大。例如,如果一个观察者抛出异常,那么接下来的观察者就不会被调用。

    static void D()
    {
        Action<int> 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<int> s = new Subject<int>();

        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 *调用(但不要随意吞噬异常,如上所示)。 对于多播委托来说也是一样的; 异常将反弹回到你这里。

大多数情况下,你可以在没有主题的情况下实现想要做的事情,例如使用Observable.Create构造新序列。 这些序列没有“观察者列表”,因此每个观察者都有自己的“会话”(冷可观察模型),因此观察者异常仅仅是在一个受限区域内的自杀命令,而不是在广场上引爆自己。

基本上,主题最好在响应查询图的边缘使用(对于需要被另一方地址化的入口流,尽管您可以使用常规的.NET事件,并使用FromEvent*方法将它们桥接到Rx中),以及在响应查询图内共享订阅时使用(使用Publish、Replay等Multicast调用伪装的主题)。 使用主题的一个危险 - 由于其观察者列表和潜在的消息记录,它们非常具有状态性质 - 是在尝试使用主题编写查询运算符时使用它们。 99.999%的情况下,这样的故事都会以悲惨结局结束。


“主题最好用于反应式查询图的边缘”是否意味着如果“DoSomething ()”是从遗留API回调的,当用户执行某些操作(例如更改音量控制)时调用,则我的使用主题是可以的。 - Wilka

12

在 Rx 论坛的一篇答案中,Rxx 的 Dave Sexton 作为回答某个问题的一部分说道:

Subjects 是 Rx 的有状态组件。当您需要将可观察事件作为字段或本地变量创建时,它们非常有用。

这正是此问题正在发生的事情,他还写了一篇深入的博客文章使用 Subject 还是不使用 Subject?,其中得出结论:

当什么情况下应该使用一个Subject?
当以下所有条件都成立时: - 没有可观察对象或者任何可以转换为可观察对象的东西。 - 需要热可观察对象。 - 可观察对象的作用域是一个类型。 - 不需要定义类似的事件且不存在类似的事件。
在这种情况下,为什么应该使用Subject?
因为你别无选择!
因此,回答“为什么在这种情况下使用Subject不好”的内部问题 - 这并不是一个坏主意,这是使用Subject的正确方式之一。

2

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

    public IObservable<T> ToObservable()
    {
        return Observable.Create<T>(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;

        });
    }

2
虽然我不能直接代表Enigmativity发言,但我想这是因为它是非常低级的,不需要直接使用;Subject<T>提供的所有内容都可以通过使用System.Reactive.Linq命名空间中的类来实现。
Subject<T>文档中的示例为例:
Subject<string> mySubject = new Subject<string>();

//*** 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);

这可以很容易地通过 Merge 扩展方法Observable 实现:
IObservable<string> feeds = 
    new NewsHeadlineFeed("Headline News Feed #1").HeadlineFeed.Merge(
    new NewsHeadlineFeed("Headline News Feed #2").HeadlineFeed);

然后您可以正常订阅它。使用Subject<T>只会使代码更复杂。如果您要使用Subject<T>,那么应该对可观察对象进行一些非常低级别的处理,而扩展方法则无法胜任。

当你想要将一个遗留库(legacy library)引入到 Rx 世界中时,该怎么办呢?我已经更新了我的问题,并提供了更具体的例子。 - Wilka

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接