.NET 针对繁忙程序员的弱事件






4.97/5 (16投票s)
通用的WeakEvent类
引言
到目前为止,我们都熟悉了 .NET 中的委托。它们非常简单但功能强大。我们使用它们进行事件、回调和许多其他美妙的派生函数。但是,委托有一个讨厌的小秘密。当您订阅一个事件时,支持该事件的委托将保持对您的强引用。这意味着什么?这意味着您(调用者)将无法被垃圾回收,因为垃圾回收算法将能够找到您。除非您喜欢内存泄漏,否则这是一个问题。本文将演示如何在不强制调用者手动取消订阅的情况下避免此问题。本文的目的是使使用弱事件变得非常简单。
背景
现在我已经看到过很多弱事件实现。它们或多或少地完成了工作,包括 WPF 中的 WeakEventManager
。我一直遇到的问题是它们的设置代码或内存使用情况。我的弱事件模式的实现将解决内存问题,并且能够匹配任何事件委托签名,同时使其非常容易地融入到您的项目中。
实现
开发人员应该了解的关于委托的第一件事是,它们与其他 .Net 类一样,也是类。它们具有我们可以用来创建自己的弱事件的属性。就像我之前提到的,委托的问题在于它们保持对调用者的强引用。我们需要做的是创建一个对调用者的弱引用。这实际上很简单。我们所要做的就是使用 WeakReference
对象来完成此操作。
RuntimeMethodHandle mInfo = delegateToMethod.Method.MethodHandle;
if (delegateToMethod.Target != null)
{
WeakReference weak = new WeakReference(delegateToMethod.Target);
subscriptions.Add(weak, mInfo);
}
else
{
staticSubscriptions.Add(mInfo);
}
在上面的示例中,'delegateToMethod
' 是我们的委托。我们可以访问它最终将调用的方法,最重要的是,我们可以访问它的目标,即订阅者。然后我们创建一个对目标的弱引用。如果目标不再在范围内,这将允许目标被垃圾回收。
我还使用其句柄在 RuntimeMethodHandle
字段中保存了指向该方法的“指针”。这样做的原因是,即使我正在创建一个对目标的弱引用,我仍然保留着一个 MethodInfo
。 MethodInfo
对象的集合将随着订阅者的增加而增长。这不节省内存。通过使用 RuntimeMethodHandler
,我实际上是在创建一个指向 MethodInfo
的指针。然后,稍后只有在我需要它们时,我才会“使它们复活”。 RuntimeMethodHandle
只有一个 IntPtr
类型的属性,它使用的内存比 MethodInfo
对象少得多。以下代码演示了它是如何工作的。
public void RaiseEvent(object[] parameters = null)
{
List<weakreference> deadTargets = new List<weakreference>();
foreach (var subcription in subscriptions)
{
object target = subcription.Key.Target;
if (target != null)
{
try
{
MethodBase.GetMethodFromHandle(subcription.Value).Invoke(target, parameters);
}
catch (Exception ex)
{
//Error("Exception caught calling delegate", ex);
}
}
else
{
deadTargets.Add(subcription.Key);
}
}
foreach (var deadTarget in deadTargets)
{
subscriptions.Remove(deadTarget);
}
}
此解决方案节省内存。但是,如果您担心性能,只需问问自己,在 99% 的情况下事件多久触发一次。
现在,我们还需要一种机制来删除委托订阅。整个功能可以封装在我的 SmartDelegate
类中。
private class SmartDelegate
{
private readonly Dictionary<<weakreference,> subscriptions = new Dictionary<weakreference,>();
private readonly List<runtimemethodhandle> staticSubscriptions = new List<runtimemethodhandle>();
#region Constructors
public SmartDelegate(Delegate delegateToMethod)
{
RuntimeMethodHandle mInfo = delegateToMethod.Method.MethodHandle;
if (delegateToMethod.Target != null)
{
WeakReference weak = new WeakReference(delegateToMethod.Target);
subscriptions.Add(weak, mInfo);
}
else
{
staticSubscriptions.Add(mInfo);
}
}
#endregion
#region Public Methods
public void RaiseEvent(object[] parameters = null)
{
List<weakreference> deadTargets = new List<weakreference>();
foreach (var subcription in subscriptions)
{
object target = subcription.Key.Target;
if (target != null)
{
try
{
MethodBase.GetMethodFromHandle(subcription.Value).Invoke(target, parameters);
}
catch (Exception ex)
{
//Error("Exception caught calling delegate", ex);
}
}
else
{
deadTargets.Add(subcription.Key);
}
}
foreach (var deadTarget in deadTargets)
{
subscriptions.Remove(deadTarget);
}
}
public bool Remove(Delegate handler)
{
WeakReference removalCandidate = null;
foreach (var subscription in subscriptions)
{
if (subscription.Key.Target != null && subscription.Key.Target == handler.Target)
{
removalCandidate = subscription.Key;
break;
}
}
if (removalCandidate != null)
{
subscriptions.Remove(removalCandidate);
return true;
}
return false;
}
#endregion
}
不用担心 Dictionary
或列表的类型,它们实际上是
private readonly Dictionary<WeakReference, RuntimeMethodHandle> subscriptions =
new Dictionary<WeakReference, RuntimeMethodHandle>();
private readonly List<RuntimeMethodHandle> staticSubscriptions = new List<RuntimeMethodHandle>();
由于某种原因,Code Project 文章编辑器不允许我将它们放入其中。
我们现在需要某种容器来管理订阅、取消订阅和引发事件的 SmartDelegates。 EventHostSubscription
类完成了这项工作
public class EventHostSubscription
{
#region Private Fields
private readonly Dictionary<string,>> subscriptions = new Dictionary<string,>>();
private int flag;
#endregion
#region Public Methods
public void Add(string eventName, Delegate handler)
{
if (handler == null)
{
throw new ArgumentNullException("handler");
}
while (Interlocked.CompareExchange(ref flag, 1, 0) != 0);
try
{
if (!subscriptions.ContainsKey(eventName))
{
subscriptions.Add(eventName, new List<smartdelegate>());
}
SmartDelegate smartDelegate = new SmartDelegate(handler);
subscriptions[eventName].Add(smartDelegate);
}
finally
{
Interlocked.Exchange(ref flag,0);
}
}
public void Remove(string eventName, Delegate handler)
{
if (handler == null)
{
throw new ArgumentNullException("handler");
}
while (Interlocked.CompareExchange(ref flag, 1, 0) != 0);
try
{
if (subscriptions.ContainsKey(eventName))
{
List<smartdelegate> smartDelegates;
if (subscriptions.TryGetValue(eventName, out smartDelegates))
{
for (int i = 0; i < smartDelegates.Count; i++)
{
SmartDelegate smartDelegate = smartDelegates[i];
smartDelegate.Remove(handler);
}
}
}
}
finally
{
Interlocked.Exchange(ref flag,0);
}
}
public void RaiseEvent(string eventName, params object[] parameters)
{
List<smartdelegate> smartDelegates;
while (Interlocked.CompareExchange(ref flag, 1, 0) != 0);
try
{
if (subscriptions.TryGetValue(eventName, out smartDelegates))
{
object[] delegateParameters = null;
if (parameters.Length > 0)
{
delegateParameters = parameters;
}
for (int i = 0; i < smartDelegates.Count; i++)
{
SmartDelegate smartDelegate = smartDelegates[i];
smartDelegate.RaiseEvent(delegateParameters);
}
}
}
finally
{
Interlocked.Exchange(ref flag,0);
}
}
}
}
同样,编辑器中字典类型声明搞砸了。下载将运行良好。
Using the Code
使用代码很简单。假设我们有一个名为 OnChanged
的事件的类,代码连接如下所示。
public class WeakEventControl
{
private readonly EventHostSubscription subscriptions = new EventHostSubscription();
public delegate void OnChangedDelegate(object sender, EventArgs args);
public event OnChangedDelegate OnChanged
{
add
{
subscriptions.Add("OnChanged", value);
}
remove
{
subscriptions.Remove("OnChanged", value);
}
}
public void RaiseEvent()
{
subscriptions.RaiseEvent("OnChanged", this, EventArgs.Empty);
}
}
就这些了。只需像往常一样使用事件的名称和参数调用 RaiseEvent
方法即可。
欢迎任何反馈或改进建议。
历史
CodeProject 成员 Sacher Barber 指出,在检查弱引用的目标是否为空和实际使用之间,目标可能被垃圾回收。我修改了我的代码来满足这一点。
CodeProject 成员 Thomas Olsson 指出,我应该“锁定” Remove 和 RaiseEvent 方法。我也修改了我的代码来满足这一点。