.NET中的弱事件?

28

如果对象A监听对象B的事件,那么对象B会让对象A保持活动状态。 是否有标准的实现弱事件的方法可以避免这种情况发生? 我知道WPF有一些机制,但我正在寻找与WPF无关的解决方案。 我猜想解决方法应该在某个地方使用弱引用。


相关问题:https://dev59.com/4nRC5IYBdhLWcg3wOeSB - Benjol
另一个相关的问题,有更好的答案:https://dev59.com/JnI-5IYBdhLWcg3wpaJ1#1747236 - Benjol
开源项目Sharp Observations(http://sharpobservation.codeplex.com/)提供了一个非常好的通用弱事件/委托实现。 - Mark
WAF开源库提供了另一种解决方法,可以在这里找到详细说明:https://github.com/jbe2277/waf/wiki/Weak-Event - jbe
9个回答

48

Dustin Campbell在DidItWith.NET博客中探讨了几种创建弱事件处理程序失败的尝试,然后展示了一个有效、可行且轻量级的实现:解决弱事件处理程序问题

不过,理想情况下,Microsoft会将这个概念引入到语言本身中。类似于:

Foo.Clicked += new weak EventHandler(...);
如果您认为这个功能对您很重要,请在此处投票支持

7
这里还有一篇关于弱事件的 CodeProject 文章: http://www.codeproject.com/KB/cs/WeakEvents.aspx - zebrabox
我也推荐这个链接:http://jp-labs.blogspot.com/2009/07/enhanced-weak-events-part-two-immutable_30.html。这是我在 codeproject 的 WeakEvents 上进行的一种优化。 - jpbochi
+1 一个“弱”关键字会让生活变得轻松得多! :) [在Microsoft Connect上投票] - devios1
3
达斯汀·坎贝尔实现弱事件的方法对调用者没有侵入性,运行效果非常好。它已经在我们的一个生产系统中运行了一段时间,零问题且性能良好。 - Kilhoffer
Dustin Campbell的博客链接已失效(404)。 - Scott Solmer

10
我重新打包了Dustin Campbell的实现,以便在不使用通用处理程序时更容易将其扩展到不同的事件类型。我认为这可能对某些人有所帮助。
荣誉归属: Mr. Campbell 的原始实现 一个非常方便的委托转换函数由 Ed Ball 提供,链接可以在源代码中找到。
处理程序和几个重载,EventHander<E> 和 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;
  }
 }
}

单元测试:


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
 }
}

这是我选择的解决方案!组装起来有些麻烦,但到目前为止它没有给我带来任何麻烦。+1 - Joel

0
Dustin的实现与WPF的WeakEventManager类相比有哪些优势?后者只是将目标对象和委托包装成弱引用。
public Listener(object target, Delegate handler)
  {
       this._target = new WeakReference(target);
       this._handler = new WeakReference((object) handler);
  }

在我看来,这种方法更加灵活,因为它不需要在调用事件处理程序时将目标实例作为参数传递给实现。
public void Invoke(object sender, E e)
        {
            T target = (T)m_TargetRef.Target;

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

这也允许使用匿名方法而不是实例方法(这似乎也是Dustin实现的一个缺点)。


0
重要细节:
达斯汀的实现消除了事件处理程序引入的强引用,但它可能会引入新的内存泄漏(至少在不够注意时是如此)。
由于注销回调未被视为弱事件处理程序,因此它可能包含对某些对象的强引用。这取决于您是否在事件订阅者类中声明了注销回调。
如果没有这样做,回调将与封闭类实例的引用相关联。这是我所指的一个示例:在声明注销回调后,它将包含对Program类实例的引用。
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);
    }
}

0

使用弱事件实现时要小心。弱事件似乎将取消订阅事件(或消息)的责任从订阅者身上移除了。在这种情况下,即使订阅者“超出范围”,事件处理程序也可能被调用。考虑一个没有明确取消订阅并且变成垃圾可回收但尚未被垃圾回收的订阅者。弱事件管理器将无法检测到该状态,因此它仍将调用该订阅者的事件处理程序。这可能会导致各种意外的副作用。

请参见弱事件模式很危险以获取更多详细信息。
查看此源代码,它使用MvvMCross消息插件作为弱事件管理器来说明此问题。


0

0

使用推荐的Dispose()模式,将事件视为需要清理的托管资源,应该可以解决这个问题。当A对象被处理时,它应该取消注册自己作为B对象事件的监听器...


2
谢谢您的回答。然而,您回答的是另一个问题。 Dispose模式并不能替代弱引用/事件。它并不是解决我的问题的方案。 - tom7
我能否告诉某人“将对象A上的此方法从其正在侦听的任何事件中断开连接”,还是我必须保留对对象B的引用? - sisve
你必须手动保留引用并从调用列表中移除它。 - Shea

0

Dustin的实现仅适用于EventHandler委托。如果您前往CodePlex,会发现一个名为 Sharp Observation的项目,作者构建了一个非常好的弱委托提供程序。它是用MSIL实现的,速度和灵活性都相当不错。

直到微软本机实现弱事件之前,这将是最佳选择。


0

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