【问题标题】:Rx - can/should I replace .NET events with Observables?Rx - 我可以/应该用 Observables 替换 .NET 事件吗?
【发布时间】:2010-08-25 16:01:52
【问题描述】:

鉴于 Reactive Extensions (Rx) framework 提供的可组合事件的好处,我想知道我的类是否应该停止推送 .NET 事件,而是公开 Rx 可观察对象。

例如,使用标准 .NET 事件获取以下类:

public class Foo
{
   private int progress;
   public event EventHandler ProgressChanged;

   public int Progress
   {
      get { return this.progress; }
      set
      {
         if (this.progress != value)
         {
            this.progress = value;

            // Raise the event while checking for no subscribers and preventing unsubscription race condition.
            var progressChanged = this.ProgressChanged;
            if (progressChanged != null)
            {
                progressChanged(this, new EventArgs());
            }
         }
      }
   }
}

很多单调的管道。

这个类可以使用某种 observable 来代替这个功能:

public class Foo
{
   public Foo()
   {
       this.Progress = some new observable;
   }

   public IObservable<int> Progress { get; private set; }
}

管道少得多。意图不再被管道细节所掩盖。这似乎是有益的。

我的问题是:

  1. 用 IObservable 值替换标准 .NET 事件是否有益/值得?
  2. 如果我要使用 observable,我会在这里使用什么类型的?显然我需要向它推送值(例如 Progress.UpdateValue(...) 或其他东西)。

【问题讨论】:

  • 如果您的代码的客户可以接受依赖 Rx,那么这对我来说似乎是个好主意。一些商店对使用仍处于实验开发阶段的软件犹豫不决,尽管我对 MS DevLabs 东西的体验是它非常稳定(我看到“生产”代码的形状要糟糕得多。)
  • 实际上,它不会强迫他们使用 Rx。 IObserver/IObservable 是标准 .NET 框架的一部分。当然,如果你想要所有性感的语法糖、扩展方法等,你会想要完整的 RX 框架。所以你在这方面是对的。感谢您的评论。

标签: c# events system.reactive


【解决方案1】:

对于#2,最直接的方法是通过主题:

Subject<int> _Progress;
IObservable<int> Progress {
    get { return _Progress; }
}

private void setProgress(int new_value) {
    _Progress.OnNext(new_value);
}

private void wereDoneWithWorking() {
    _Progress.OnCompleted();
}

private void somethingBadHappened(Exception ex) {
    _Progress.OnError(ex);
}

有了这个,现在你的“进度”不仅可以通知进度何时发生变化,还可以通知操作何时完成,以及是否成功。但请记住,一旦 IObservable 通过 OnCompleted 或 OnError 完成,它就“死”了——你不能再向它发布任何内容。

【讨论】:

  • 有趣。我对主题不熟悉。我认为这是我需要的,只要能够更新它。不过,我不喜欢 foo.SetProgress(42) 调用。我宁愿做类似 foo.Progress.Value = 42;也许我在这里真正需要一个自定义的 Observable。
  • 我想我对使用主题的反对归结为在被调用者身上投入了更多的工作——现在我必须为我的类中的每个可观察对象提供一个单独的 setter 方法。我现在肯定倾向于自定义观察者。
  • 你没有使用单独的设置器,我只是想让它更清楚 - 如果你希望你的班级直接摆弄主题,你可以这样做那也是
  • 添加 .AsObservable();到你的 Progress 属性的 getter:return _Progress.AsObservable();。将阻止消费者将返回的 IObservable 转换为 Subject,然后能够在您的内部 Subject 上调用 OnNext、OnError 或 OnCompleted。可以在here 找到一个简短的讨论。
  • 我并不特别担心——这就像担心人们会使用反射来访问私有字段一样,我认为我的代码的客户不会主动找我
【解决方案2】:

如果有内置主题可以为您做到这一点,我不建议您管理自己的订阅者列表。它还消除了携带您自己的可变 T 副本的需要。

以下是我的(无评论)版本的解决方案:

public class Observable<T> : IObservable<T>, INotifyPropertyChanged 
{ 
    private readonly BehaviorSubject<T> values; 

    private PropertyChangedEventHandler propertyChanged; 

    public Observable() : this(default(T))
    {
    } 

    public Observable(T initalValue) 
    { 
        this.values = new BehaviorSubject<T>(initalValue);

        values.DistinctUntilChanged().Subscribe(FirePropertyChanged);
    }

    public T Value 
    { 
        get { return this.values.First(); } 
        set { values.OnNext(value); } 
    }

    private void FirePropertyChanged(T value)
    {
        var handler = this.propertyChanged;

        if (handler != null)
            handler(this, new PropertyChangedEventArgs("Value"));
    }

    public override string ToString() 
    { 
        return value != null ? value.ToString() : "Observable<" + typeof(T).Name + "> with null value."; 
    } 

    public static implicit operator T(Observable<T> input) 
    { 
        return input.Value; 
    } 

    public IDisposable Subscribe(IObserver<T> observer) 
    { 
        return values.Subscribe(observer);
    } 

    event PropertyChangedEventHandler INotifyPropertyChanged.PropertyChanged 
    { 
        add { this.propertyChanged += value; } 
        remove { this.propertyChanged -= value; } 
    } 
}

【讨论】:

  • 更新了您的代码以删除 DistinctUntilChanged 订阅中的“this.value = x”行,因为没有值字段。
【解决方案3】:

我会尽量简短:

  1. 是的
  2. 行为主题

:)

【讨论】:

    【解决方案4】:

    好的,伙计们,我认为至少值得一试,并且看到 RX 的 Subject 不是我想要的,我创建了一个新的 observable 适合我的需求:

    • 实现 IObservable
    • 实现 INotifyPropertyChange 以使用 WPF/Silverlight 绑定。​​
    • 提供简单的 get/set 语义。

    我称这个类为 Observable

    声明:

    /// <summary>
    /// Represents a value whose changes can be observed.
    /// </summary>
    /// <typeparam name="T">The type of value.</typeparam>
    public class Observable<T> : IObservable<T>, INotifyPropertyChanged
    {
        private T value;
        private readonly List<AnonymousObserver> observers = new List<AnonymousObserver>(2);
        private PropertyChangedEventHandler propertyChanged;
    
        /// <summary>
        /// Constructs a new observable with a default value.
        /// </summary>
        public Observable()
        {
        }
    
        public Observable(T initalValue)
        {
            this.value = initialValue;
        }
    
        /// <summary>
        /// Gets the underlying value of the observable.
        /// </summary>
        public T Value
        {
            get { return this.value; }
            set
            {
                var valueHasChanged = !EqualityComparer<T>.Default.Equals(this.value, value);
                this.value = value;
    
                // Notify the observers of the value.
                this.observers
                    .Select(o => o.Observer)
                    .Where(o => o != null)
                    .Do(o => o.OnNext(value))
                    .Run();
    
                // For INotifyPropertyChange support, useful in WPF and Silverlight.
                if (valueHasChanged && propertyChanged != null)
                {
                   propertyChanged(this, new PropertyChangedEventArgs("Value"));
                }
            }
        }
    
        /// <summary>
        /// Converts the observable to a string. If the value isn't null, this will return
        /// the value string.
        /// </summary>
        /// <returns>The value .ToString'd, or the default string value of the observable class.</returns>
        public override string ToString()
        {
            return value != null ? value.ToString() : "Observable<" + typeof(T).Name + "> with null value.";
        }
    
        /// <summary>
        /// Implicitly converts an Observable to its underlying value.
        /// </summary>
        /// <param name="input">The observable.</param>
        /// <returns>The observable's value.</returns>
        public static implicit operator T(Observable<T> input)
        {
            return input.Value;
        }
    
        /// <summary>
        /// Subscribes to changes in the observable.
        /// </summary>
        /// <param name="observer">The subscriber.</param>
        /// <returns>A disposable object. When disposed, the observer will stop receiving events.</returns>
        public IDisposable Subscribe(IObserver<T> observer)
        {
            var disposableObserver = new AnonymousObserver(observer);
            this.observers.Add(disposableObserver);
            return disposableObserver;
        }
    
        event PropertyChangedEventHandler INotifyPropertyChanged.PropertyChanged
        {
            add { this.propertyChanged += value; }
            remove { this.propertyChanged -= value; }
        }
    
        class AnonymousObserver : IDisposable
        {
            public IObserver<T> Observer { get; private set; }
    
            public AnonymousObserver(IObserver<T> observer)
            {
                this.Observer = observer;
            }
    
            public void Dispose()
            {
                this.Observer = null;
            }
        }
    }
    

    用法:

    消费很简单。没有管道!

    public class Foo
    {
        public Foo()
        {
            Progress = new Observable<T>();
        } 
    
        public Observable<T> Progress { get; private set; }
    }
    

    用法很简单:

    // Getting the value works just like normal, thanks to implicit conversion.
    int someValue = foo.Progress;
    
    // Setting the value is easy, too:
    foo.Progress.Value = 42;
    

    您可以在 WPF 或 Silverlight 中对其进行数据绑定,只需绑定到 Value 属性即可。

    <ProgressBar Value={Binding Progress.Value} />
    

    最重要的是,您可以使用 IObservables 编写、过滤、投影和做所有 RX 允许您做的性感事情:

    过滤事件:

    foo.Progress
       .Where(val => val == 100)
       .Subscribe(_ => MyProgressFinishedHandler());
    

    N 次调用后自动取消订阅:

    foo.Progress
       .Take(1)
       .Subscribe(_ => OnProgressChangedOnce());
    

    编写事件:

    // Pretend we have an IObservable<bool> called IsClosed:
    foo.Progress
       .TakeUntil(IsClosed.Where(v => v == true))
       .Subscribe(_ => ProgressChangedWithWindowOpened());
    

    好东西!

    【讨论】:

    • 由于线程问题等原因,我不建议您自己管理主题。请参阅我的答案
    • 我绝对支持 Richard 的评论和回答,并补充说主题比您的解决方案优化得多(除了线程安全之外)
    【解决方案5】:

    除了您现有的事件代码可能更简洁之外:

        public event EventHandler ProgressChanged = delegate {};
    
        ...
           set {
              ... 
              // no need for null check anymore       
              ProgressChanged(this, new EventArgs());
       }
    

    我认为通过切换到Observable&lt;int&gt;,您只是将复杂性从被调用者转移到调用者。如果我只想读取 int 怎么办?

    -奥辛

    【讨论】:

    • 这有在堆上创建一个额外的对象的缺点,以及提高 ProgressChanged 即使进度确实没有改变。此外,我失去了使用标准事件的可组合性。这实际上给调用者带来了很大的负担,例如只订阅一个活动?调用者必须跟踪事件并在调用后取消订阅。处理程序超出范围?调用者必须确保解除事件挂钩,否则我们会发生内存泄漏。想要将 2 个事件组合在一起,例如 MouseDown 和 MouseMove 以进行拖放?来电者必须协调整个混乱。可组合性很好。 Rx 让一切变得简单。
    • 要读取 int,我会提供一些自定义的 observable,以便您可以说 Progress.Value 或其他内容。
    • @Judah - 我想这取决于你的听众,更重要的是,你班级的总体目的和消费者的需求。顺便说一句,您的原始代码也有一个竞争条件,它检查委托是否为空;您应该在本地获取 ProgressChanged 的​​副本并测试它是否为空。我没有费心指出它,但如果你坚持谈论堆等,我想我应该提出来;-)
    • 我知道这种竞争条件,请在此处查看我的解决方法:stackoverflow.com/questions/248072/… 尽管如此,对该竞争条件的正确检查只会增加更多单调的管道。呸!我越来越喜欢 Rx。我只是想听听 RX 社区对此的看法。
    • @Judah - 当然。我同意,比赛条件只会支持你的情况。我相信 RX 社区会说“是的,去吧”,但就像我说的,Observables 比简单的事件更复杂,所以你的类的消费者可能更重要。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2020-02-07
    • 1970-01-01
    • 2020-02-04
    • 2018-06-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多