Weak events in .NET?

2019-01-13 05:19发布

问题:

If object A listens to an event from object B, object B will keep object A alive. Is there a standard implementation of weak events that would prevent this? I know WPF has some mechanism but I am looking for something not tied to WPF. I am guessing the solution should use weak references somewhere.

回答1:

Dustin Campbell from the DidItWith.NET blog examines several of the failed attempts to create weak event handlers, then goes on to show a valid, working, lightweight implementation: Solving the Problem With Weak Event Handlers.

Ideally, though, Microsoft would introduce the concept into the language itself. Something like:

Foo.Clicked += new weak EventHandler(...);

If you feel this feature is important to you, please vote for it here.



回答2:

I repackaged Dustin Campbell's implementation to make it a little easier to extend it for different event types when generic handlers aren't used. I figure it may be of some use to someone.

Credits:
Mr. Campbell's original implementation
A very handy delegate cast function by Ed Ball, link can be found in source

The handler and a couple of overloads, EventHander<E> and PropertyChangedEventHandler:


///  Basic weak event management. 
/// 
///  Weak allow objects to be garbage collected without having to unsubscribe
///  
///  Taken with some minor variations from:
///  http://diditwith.net/2007/03/23/SolvingTheProblemWithEventsWeakEventHandlers.aspx
///  
///  use as class.theEvent +=new EventHandler<EventArgs>(instance_handler).MakeWeak((e) => class.theEvent -= e);
///  MakeWeak extension methods take an delegate to unsubscribe the handler from the event
/// 

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Reflection;
using System.Text;

namespace utils {

 /// <summary>
 /// Delegate of an unsubscribe delegate
 /// </summary>
 public delegate void UnregisterDelegate<H>(H eventHandler) where H : class;

 /// <summary>
 /// A handler for an event that doesn't store a reference to the source
 /// handler must be a instance method
 /// </summary>
 /// <typeparam name="T">type of calling object</typeparam>
 /// <typeparam name="E">type of event args</typeparam>
 /// <typeparam name="H">type of event handler</typeparam>
 public class WeakEventHandlerGeneric<T, E, H>
  where T : class
  where E : EventArgs 
  where H : class {

  private delegate void OpenEventHandler(T @this, object sender, E e);

  private delegate void LocalHandler(object sender, E e);

  private WeakReference m_TargetRef;
  private OpenEventHandler m_OpenHandler;
  private H m_Handler;
  private UnregisterDelegate<H> m_Unregister;

  public WeakEventHandlerGeneric(H eventHandler, UnregisterDelegate<H> unregister) {
   m_TargetRef = new WeakReference((eventHandler as Delegate).Target);
   m_OpenHandler = (OpenEventHandler)Delegate.CreateDelegate(typeof(OpenEventHandler), null, (eventHandler as Delegate).Method);
   m_Handler = CastDelegate(new LocalHandler(Invoke));
   m_Unregister = unregister;
  }

  private void Invoke(object sender, E e) {
   T target = (T)m_TargetRef.Target;

   if (target != null)
    m_OpenHandler.Invoke(target, sender, e);
   else if (m_Unregister != null) {
    m_Unregister(m_Handler);
    m_Unregister = null;
   }
  }

  /// <summary>
  /// Gets the handler.
  /// </summary>
  public H Handler {
   get { return m_Handler; }
  }

  /// <summary>
  /// Performs an implicit conversion from <see cref="PR.utils.WeakEventHandler&lt;T,E&gt;"/> to <see cref="System.EventHandler&lt;E&gt;"/>.
  /// </summary>
  /// <param name="weh">The weh.</param>
  /// <returns>The result of the conversion.</returns>
  public static implicit operator H(WeakEventHandlerGeneric<T, E, H> weh) {
   return weh.Handler;
  }

  /// <summary>
  /// Casts the delegate.
  /// Taken from
  /// http://jacobcarpenters.blogspot.com/2006/06/cast-delegate.html
  /// </summary>
  /// <param name="source">The source.</param>
  /// <returns></returns>
  public static H CastDelegate(Delegate source) {
   if (source == null) return null;

   Delegate[] delegates = source.GetInvocationList();
   if (delegates.Length == 1)
    return Delegate.CreateDelegate(typeof(H), delegates[0].Target, delegates[0].Method) as H;

   for (int i = 0; i < delegates.Length; i++)
    delegates[i] = Delegate.CreateDelegate(typeof(H), delegates[i].Target, delegates[i].Method);

   return Delegate.Combine(delegates) as H;
  }
 }

 #region Weak Generic EventHandler<Args> handler

 /// <summary>
 /// An interface for a weak event handler
 /// </summary>
 /// <typeparam name="E"></typeparam>
 public interface IWeakEventHandler<E> where E : EventArgs {
  EventHandler<E> Handler { get; }
 }

 /// <summary>
 /// A handler for an event that doesn't store a reference to the source
 /// handler must be a instance method
 /// </summary>
 /// <typeparam name="T"></typeparam>
 /// <typeparam name="E"></typeparam>
 public class WeakEventHandler<T, E> : WeakEventHandlerGeneric<T, E, EventHandler<E>>, IWeakEventHandler<E>
  where T : class
  where E : EventArgs {

  public WeakEventHandler(EventHandler<E> eventHandler, UnregisterDelegate<EventHandler<E>> unregister) 
   : base(eventHandler, unregister) { }
 }

 #endregion

 #region Weak PropertyChangedEvent handler

 /// <summary>
 /// An interface for a weak event handler
 /// </summary>
 /// <typeparam name="E"></typeparam>
 public interface IWeakPropertyChangedEventHandler {
  PropertyChangedEventHandler Handler { get; }
 }

 /// <summary>
 /// A handler for an event that doesn't store a reference to the source
 /// handler must be a instance method
 /// </summary>
 /// <typeparam name="T"></typeparam>
 /// <typeparam name="E"></typeparam>
 public class WeakPropertyChangeHandler<T> : WeakEventHandlerGeneric<T, PropertyChangedEventArgs, PropertyChangedEventHandler>, IWeakPropertyChangedEventHandler
  where T : class {

  public WeakPropertyChangeHandler(PropertyChangedEventHandler eventHandler, UnregisterDelegate<PropertyChangedEventHandler> unregister) 
   : base(eventHandler, unregister) {}
 }

 #endregion

 /// <summary>
 /// Utilities for the weak event method
 /// </summary>
 public static class WeakEventExtensions {

  private static void CheckArgs(Delegate eventHandler, Delegate unregister) {
   if (eventHandler == null) throw new ArgumentNullException("eventHandler");
   if (eventHandler.Method.IsStatic || eventHandler.Target == null) throw new ArgumentException("Only instance methods are supported.", "eventHandler");
  }

  private static object GetWeakHandler(Type generalType, Type[] genericTypes, Type[] constructorArgTypes, object[] constructorArgs) {
   var wehType = generalType.MakeGenericType(genericTypes);
   var wehConstructor = wehType.GetConstructor(constructorArgTypes);
   return wehConstructor.Invoke(constructorArgs);
  }

  /// <summary>
  /// Makes a property change handler weak
  /// </summary>
  /// <typeparam name="E"></typeparam>
  /// <param name="eventHandler">The event handler.</param>
  /// <param name="unregister">The unregister.</param>
  /// <returns></returns>
  public static PropertyChangedEventHandler MakeWeak(this PropertyChangedEventHandler eventHandler, UnregisterDelegate<PropertyChangedEventHandler> unregister) {
   CheckArgs(eventHandler, unregister);

   var generalType = typeof (WeakPropertyChangeHandler<>);
   var genericTypes = new[] {eventHandler.Method.DeclaringType};
   var constructorTypes = new[] { typeof(PropertyChangedEventHandler), typeof(UnregisterDelegate<PropertyChangedEventHandler>) };
   var constructorArgs = new object[] {eventHandler, unregister};

   return ((IWeakPropertyChangedEventHandler) GetWeakHandler(generalType, genericTypes, constructorTypes, constructorArgs)).Handler;
  }

  /// <summary>
  /// Makes a generic handler weak
  /// </summary>
  /// <typeparam name="E"></typeparam>
  /// <param name="eventHandler">The event handler.</param>
  /// <param name="unregister">The unregister.</param>
  /// <returns></returns>
  public static EventHandler<E> MakeWeak<E>(this EventHandler<E> eventHandler, UnregisterDelegate<EventHandler<E>> unregister) where E : EventArgs {
   CheckArgs(eventHandler, unregister);

   var generalType = typeof(WeakEventHandler<,>);
   var genericTypes = new[] { eventHandler.Method.DeclaringType, typeof(E) };
   var constructorTypes = new[] { typeof(EventHandler<E>), typeof(UnregisterDelegate<EventHandler<E>>) };
   var constructorArgs = new object[] { eventHandler, unregister };

   return ((IWeakEventHandler<E>)GetWeakHandler(generalType, genericTypes, constructorTypes, constructorArgs)).Handler;
  }
 }
}

Unit tests:


using System.ComponentModel;
using NUnit.Framework;
using System.Collections.Generic;
using System;

namespace utils.Tests {
 [TestFixture]
 public class WeakEventTests {

  #region setup/teardown

  [TestFixtureSetUp]
  public void SetUp() {
   testScenarios.Add(SetupTestGeneric);
   testScenarios.Add(SetupTestPropChange);
  }

  [TestFixtureTearDown]
  public void TearDown() {

  }

  #endregion

  #region tests

  private List<Action<bool>> testScenarios = new List<Action<bool>>();

  private IEventSource source;
  private WeakReference sourceRef;

  private IEventConsumer consumer;
  private WeakReference consumerRef;

  private IEventConsumer consumer2;
  private WeakReference consumerRef2;

  [Test]
  public void ConsumerSourceTest() {
   foreach(var a in testScenarios) {
    a(false);
    ConsumerSourceTestMethod();
   }
  }

  private void ConsumerSourceTestMethod() {
   Assert.IsFalse(consumer.eventSet);
   source.Fire();
   Assert.IsTrue(consumer.eventSet);
  }

  [Test]
  public void ConsumerLinkTest() {
   foreach (var a in testScenarios) {
    a(false);
    ConsumerLinkTestMethod();
   }
  }

  private void ConsumerLinkTestMethod() {
   consumer = null;
   GC.Collect();
   Assert.IsFalse(consumerRef.IsAlive);
   Assert.IsTrue(source.InvocationCount == 1);
   source.Fire();
   Assert.IsTrue(source.InvocationCount == 0);
  }

  [Test]
  public void ConsumerLinkTestDouble() {
   foreach (var a in testScenarios) {
    a(true);
    ConsumerLinkTestDoubleMethod();
   }
  }

  private void ConsumerLinkTestDoubleMethod() {
   consumer = null;
   GC.Collect();
   Assert.IsFalse(consumerRef.IsAlive);
   Assert.IsTrue(source.InvocationCount == 2);
   source.Fire();
   Assert.IsTrue(source.InvocationCount == 1);
   consumer2 = null;
   GC.Collect();
   Assert.IsFalse(consumerRef2.IsAlive);
   Assert.IsTrue(source.InvocationCount == 1);
   source.Fire();
   Assert.IsTrue(source.InvocationCount == 0);
  }

  [Test]
  public void ConsumerLinkTestMultiple() {
   foreach (var a in testScenarios) {
    a(true);
    ConsumerLinkTestMultipleMethod();
   }
  }

  private void ConsumerLinkTestMultipleMethod() {
   consumer = null;
   consumer2 = null;
   GC.Collect();
   Assert.IsFalse(consumerRef.IsAlive);
   Assert.IsFalse(consumerRef2.IsAlive);
   Assert.IsTrue(source.InvocationCount == 2);
   source.Fire();
   Assert.IsTrue(source.InvocationCount == 0);
  }

  [Test]
  public void SourceLinkTest() {
   foreach (var a in testScenarios) {
    a(false);
    SourceLinkTestMethod();
   }
  }

  private void SourceLinkTestMethod() {
   source = null;
   GC.Collect();
   Assert.IsFalse(sourceRef.IsAlive);
  }

  [Test]
  public void SourceLinkTestMultiple() {
   SetupTestGeneric(true);
   foreach (var a in testScenarios) {
    a(true);
    SourceLinkTestMultipleMethod();
   }
  }

  private void SourceLinkTestMultipleMethod() {
   source = null;
   GC.Collect();
   Assert.IsFalse(sourceRef.IsAlive);
  }

  #endregion

  #region test helpers

  public void SetupTestGeneric(bool both) {
   source = new EventSourceGeneric();
   sourceRef = new WeakReference(source);

   consumer = new EventConsumerGeneric((EventSourceGeneric)source);
   consumerRef = new WeakReference(consumer);

   if (both) {
    consumer2 = new EventConsumerGeneric((EventSourceGeneric)source);
    consumerRef2 = new WeakReference(consumer2);
   }
  }

  public void SetupTestPropChange(bool both) {
   source = new EventSourcePropChange();
   sourceRef = new WeakReference(source);

   consumer = new EventConsumerPropChange((EventSourcePropChange)source);
   consumerRef = new WeakReference(consumer);

   if (both) {
    consumer2 = new EventConsumerPropChange((EventSourcePropChange)source);
    consumerRef2 = new WeakReference(consumer2);
   }
  }

  public interface IEventSource {
   int InvocationCount { get; }
   void Fire();
  }

  public class EventSourceGeneric : IEventSource {
   public event EventHandler<EventArgs> theEvent;
   public int InvocationCount {
    get { return (theEvent != null)? theEvent.GetInvocationList().Length : 0; }
   }
   public void Fire() {
    if (theEvent != null) theEvent(this, EventArgs.Empty);
   }
  }

  public class EventSourcePropChange : IEventSource {
   public event PropertyChangedEventHandler theEvent;
   public int InvocationCount {
    get { return (theEvent != null) ? theEvent.GetInvocationList().Length : 0; }
   }
   public void Fire() {
    if (theEvent != null) theEvent(this, new PropertyChangedEventArgs(""));
   }
  }

  public interface IEventConsumer {
   bool eventSet { get; }
  }

  public class EventConsumerGeneric : IEventConsumer {
   public bool eventSet { get; private set; }
   public EventConsumerGeneric(EventSourceGeneric sourceGeneric) {
    sourceGeneric.theEvent +=new EventHandler<EventArgs>(source_theEvent).MakeWeak((e) => sourceGeneric.theEvent -= e);
   }
   public void source_theEvent(object sender, EventArgs e) {
    eventSet = true;
   }
  }

  public class EventConsumerPropChange : IEventConsumer {
   public bool eventSet { get; private set; }
   public EventConsumerPropChange(EventSourcePropChange sourcePropChange) {
    sourcePropChange.theEvent += new PropertyChangedEventHandler(source_theEvent).MakeWeak((e) => sourcePropChange.theEvent -= e);
   }
   public void source_theEvent(object sender, PropertyChangedEventArgs e) {
    eventSet = true;
   }
  }

  #endregion
 }
}


回答3:

Using the recommended Dispose() pattern, where you consider events a managed resource to clean up, should handle this. Object A should unregister itself as a listener of events from object B when it's disposed...



回答4:

Dustin Campbell's approach is already excellent. The only thing left, save a solution integrated into .NET, is a really simple way to create really generic weak event handlers:

http://puremsil.wordpress.com/2010/05/03/generic-weak-event-handlers/



回答5:

Dustin's implementation only works with EventHandler delegates. If you head over to CodePlex there's a project called Sharp Observation in which the author has built a very good weak delegate provider. It's implemented in MSIL and is considerably faster and more flexible.

... which, until Microsoft implement weak events natively, will have to do.



回答6:

There's also a solution that works in Silverlight/WP7 which uses Linq expressions instead of MSIL emit.

http://socialeboladev.wordpress.com/2012/09/23/weak-event-implementation-that-works-for-any-event-type/



回答7:

An important detail:

Dustin's implementation eliminates a strong reference introduced by the event handler, but it may introduce a new memory leak (at least when not paying enough attention to).

Since the unregister callback is not treated as a weak event handler it may contain a strong reference to some object. It depends on whether you declare the unregister callback in the Event subscriber class or not.

If not doing so, the callback will be associated with a reference to the enclosing class instance. Here is an example of what I am refering to: After declaring the unregister callback it will contain a reference to the Program class instance:

public class EventSource
        {
            public event EventHandler<EventArgs> Fired
        }
}
 public class EventSubscriber
    {
        public void OnEventFired(object sender, EventArgs) { ; }
    }

 public class Program {

    public void Main()
    {
    var source = new EventSource();
    var subscriber = new EventSubscriber();
    source.Fired += new WeakEventHandler<EventSubscriber, EventArgs>(subscriber.OnEventFired, handler => source.Fired -= handler);
    }
}


回答8:

What advantages does Dustin's implementation have compared to the WPF's WeakEventManager class which simply wraps the target object as well as the delegate into a weak reference:

public Listener(object target, Delegate handler)
  {
       this._target = new WeakReference(target);
       this._handler = new WeakReference((object) handler);
  }

In my opinion this approach is more flexible, since it does not require the implementation to pass the target instance as a parameter during the invocation of the event handler:

public void Invoke(object sender, E e)
        {
            T target = (T)m_TargetRef.Target;

            if (target != null)
                m_OpenHandler(target, sender, e);

This also allows the use of anomymous methods instead of an instance method (that seems to be also a disadvantage of the Dustin's implementation).



回答9:

Be careful when using weak events implementations. Weak events appear to remove from the subscriber the responsibility of unsubscribing from the event (or message). In that case event handlers may be invoked even after the subscriber "goes out of scope". Take a subscriber that does not explicitly unsubscribe and that becomes garbage collectable but is not yet garbage collected. The weak event manager will not be able to detect that state and because of that it will still call the event handler of that subscriber. This can lead to all kind of unexpected side effects.

See more details at The Weak Event Pattern is Dangerous.
See this source code that illustrates this issue using the MvvMCross messaging plugin as a weak event manager.