有没有一种方法可以让C#订阅对象在不直接知道订阅对象的情况下取消订阅事件

本文关键字:对象 事件 取消 情况下 方法 一种 有没有 | 更新日期: 2023-09-27 18:28:42

我有一个用户控件,它将inner对象列表订阅到另一个组件中的对象。有时我想根据用户控件的属性设置更改来删除inner对象。如果我在不取消订阅的情况下盲目地将它们从列表中删除,我认为这将导致孤立inner对象的内存泄漏。

有时我可能无法访问我们订阅时的对象列表。例如,我订阅的项目列表可能已更改,这也是我取消订阅的原因之一。

我当然可以在代码中为每个内部对象添加对订阅对象的引用,但我很感兴趣的是,看看是否有一个内置机制可以利用。

更新#1:汉斯要求提供一些代码。我制作了一个样本,我认为该样本显示,当列表被清除时,innerObject没有发布。我认为这相当于将它们的引用设置为null。

/*
 * Program that demonstrates that event subscribers stay alive after
 * loosing scope.
 * 
 * The question is asking if I don't have a reference to myObject is there a way to 
 * unsubscribe with what the innerObject knows natively.
 * 
 * In the Unsubscribe() method I have examples of unsubscribing using a "known" myObject
 * and one with a self reference to the myObject.
 * 
 * The solution that uses IObserver and IObservable automates a way to store an explicit
 * reference to the subscription holder.
 * 
 */
using System;
using System.Collections.Generic;
namespace ConsoleApplication1
{
  /// <summary>
  /// Sample objects that subscribe to MyObject's Updated event
  /// </summary>
  public class InnerObject
  {
    public MyObject Subscribed { get; set; }
    public void Updated(object sender, MyObjectUpdateEventArgs e)
    {
      Console.WriteLine(e.Data);
    }
  }
  /// <summary>
  /// Object that publishes the Updated event
  /// </summary>
  public class MyObject
  {
    private string data_;
    public event EventHandler<MyObjectUpdateEventArgs> Updated;
    public string Data { get { return data_; } set { SetMyData(value); } }
    /// <summary>
    /// Outputs the Data string
    /// </summary>
    /// <returns></returns>
    public override string ToString()
    {
      return Data;
    }
    /// <summary>
    /// Event handler
    /// </summary>
    /// <param name="e"></param>
    private void OnUpdate(MyObjectUpdateEventArgs e)
    {
      EventHandler<MyObjectUpdateEventArgs> handler = Updated;
      if (Updated != null) {
        handler(this, e);
      }
    }
    private void SetMyData(string value)
    {
      if (Data != value) {
        data_ = value;
        OnUpdate(new MyObjectUpdateEventArgs(Data));
      }
    }
  }
  /// <summary>
  /// EventArgs to provide updated MyData value;
  /// </summary>
  public class MyObjectUpdateEventArgs
  {
    public MyObjectUpdateEventArgs(string data)
    {
      Data = data;
    }
    public string Data { get; set; }
  }
  internal class Program
  {
    private static List<InnerObject> innerObjectsList = new List<InnerObject>();
    private static void Main(string[] args)
    {
      MyObject myObject = new MyObject();
      myObject.Data = "Hello World";
      Console.WriteLine(myObject.ToString());
      Console.ReadLine();
      // Create the innerObjectts and subscribe
      MakeNewInnerObjects(5, myObject);
      // This will cause all of the inner objects to respond with myObjects Data string
      Console.WriteLine("Assigning new data to myObject'n");
      myObject.Data = "Hello InnerObjects";
      // Shows they are responding, even though the list and the items are out of scope
      Console.ReadLine();
      // Uncomment to unsubscribe
      // Unsubscribe(myObject);
      innerObjectsList.Clear();
      // Force garbage collection for our example
      GC.Collect();
      GC.WaitForPendingFinalizers();
      Console.WriteLine("Assigning new data to myObject'n");
      myObject.Data = "Are you still there InnerObjects?";
      // Shows they still exist if we don't unsubscribe
      Console.ReadLine();
    }
    /// <summary>
    /// Create a count of InnerObjects, subscribe them to myObject and add to the list
    /// </summary>
    /// <param name="count"></param>
    private static void MakeNewInnerObjects(int count, MyObject myObject)
    {
      // Uncomment if you want to have the list go out of scope as well to show
      // That the reference keeps them alive
      //List<InnerObject> innerObjectsList = new List<InnerObject>();
      for (int i = 0; i < count; i++) {
        InnerObject innerObject = new InnerObject();
        innerObject.Subscribed = myObject;
        myObject.Updated += innerObject.Updated;
        innerObjectsList.Add(innerObject);
      }
    }
    // Unsubscribe the list of innerObJects from MyObject
    private static void Unsubscribe(MyObject myObject)
    {
      // Two ways to unsubscribe, the first depends on knowing what we subscribbed to
      // the second uses a stored reference to the object
      foreach (InnerObject innerObject in innerObjectsList) {
        // myObject.Updated -= innerObject.Updated;
        innerObject.Subscribed.Updated -= innerObject.Updated;
      }
    }
  }
}

有没有一种方法可以让C#订阅对象在不直接知道订阅对象的情况下取消订阅事件

如果您为发布者实现IObservable,那么Subscribe调用将返回一个订阅对象,该对象将在您销毁它时取消订阅

public class Observable<T> : IObservable<T>
{
    protected readonly List<IObserver<T>> _subscribers = new List<IObserver<T>>();
    private class Subscription : IDisposable
    {
        List<IObserver<T>> _subscribers;
        IObserver<T> _observer;
        public Subscription(List<IObserver<T>> subscribers, IObserver<T> observer)
        {
            _subscribers = subscribers;
            _observer = observer;
        }
        public void Dispose()
        {
            _subscribers.Remove(_observer);
        }
    }
    public IDisposable Subscribe(IObserver<T> observer)
    {
        _subscribers.Add(observer);
        return new Subscription(_subscribers, observer);
    }
}

Subscription类完成断开连接的所有工作。难题的另一面是订户:

public abstract class Observer<T> : IObserver<T>, IDisposable
{
    private IDisposable _subscription = null;
    public void Dispose()
    {
        Unsubscribe();
    }
    public void Unsubscribe()
    {
        if (_subscription != null)
        {
            _subscription.Dispose();
            _subscription = null;
        }
    }
    public void SubscribeTo(IObservable<T> publisher)
    {
        Unsubscribe();
        _subscription = publisher.Subscribe(this);
    }
    public virtual void OnCompleted()
    { }
    public abstract void OnError(Exception error)
    { }
    public abstract void OnNext(T value);
}

即使您不使用IObservable<T>IObserver<T>,基本原理也是相同的。您可以跟踪发布服务器/IObservable上的订阅服务器,并分发可以从发布服务器列表中删除该订阅服务器的订阅服务器。然后,订阅者只需要跟踪他们的订阅。


更新:这里有一个以上类的用法示例。

首先,在Observable<T>类中添加一个方便的函数:

public virtual void Publish(T value)
{
    foreach (var sub in _subscribers.Distinct().ToArray())
    {
        try
        {
            sub.OnNext(value);
        }
        catch { }
    }
}

这提供了一个简单的方法来通知所有订阅者发生了一些事情。现在,一个略显做作的例子:

public class KeyPublisher : Observable<ConsoleKeyInfo>
{
}
public class PrintKeys : Observer<ConsoleKeyInfo>
{
    public override void OnNext(ConsoleKeyInfo next)
    {
        if (next.Modifiers != 0)
            Console.Write("{0}-", next.Modifiers.ToString().Replace(", ", "-"));
        Console.WriteLine(next.Key);
    }
}
public class DetectEscape : Observer<ConsoleKeyInfo>
{
    public bool FoundEscape { get; private set; }
    public override void OnNext(ConsoleKeyInfo next)
    {
        if (next.Key == ConsoleKey.Escape)
            FoundEscape = true;
    }
}
class Program
{
    static void Main(string[] args)
    {
        var pub = new KeyPublisher();
        using (var sub1 = new PrintKeys())
        using (var sub2 = new DetectEscape())
        {
            sub1.SubscribeTo(pub);
            sub2.SubscribeTo(pub);
            while (!sub2.FoundEscape)
            {
                pub.Publish(Console.ReadKey(true));
            }
        }
    }
}

如果由于各种列表和订阅持有引用而导致对象寿命出现问题,解决方案可能是使用WeakReferences。下面是使用弱引用的Subscription类的一个版本:

private class Subscription : IDisposable
{
    WeakReference<List<IObserver<T>>> _subscribers;
    WeakReference<IObserver<T>> _observer;
    public Subscription(List<IObserver<T>> subscribers, IObserver<T> observer)
    {
        _subscribers = new WeakReference<List<IObserver<T>>>(subscribers);
        _observer = new WeakReference<IObserver<T>>(observer);
    }
    public void Dispose()
    {
        if (_subscribers != null && _observer != null)
        {
            List<IObserver<T>> subscribers;
            IObserver<T> observer;
            if (_subscribers.TryGetTarget(out subscribers) && _observer.TryGetTarget(out observer))
                subscribers.Remove(observer);
            _subscribers = null;
            _observer = null;
        }
    }
}

您可以对_subscribers列表执行同样的操作(使其保持WeakReference<IObserver<T>>),以阻止其阻止订阅服务器进行垃圾收集。。。但我认为管理IDisposable对象的生命周期是更好的做法。