有没有办法让C#订阅对象取消订阅事件,而不直接知道它订阅的对象?

时间:2016-01-03 01:36:14

标签: c# events memory-leaks

我有一个用户控件,它将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;
      }
    }
  }
}

1 个答案:

答案 0 :(得分:1)

如果您为发布商实施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));
            }
        }
    }
}

如果由于各种列表和订阅持有引用而导致对象生命周期出现问题,则解决方案可能是使用WeakReference。这是使用弱引用的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个对象的生命周期。