65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.97/5 (16投票s)

2013年2月1日

CPOL

3分钟阅读

viewsIcon

68139

downloadIcon

624

通用的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 字段中保存了指向该方法的“指针”。这样做的原因是,即使我正在创建一个对目标的弱引用,我仍然保留着一个 MethodInfoMethodInfo 对象的集合将随着订阅者的增加而增长。这不节省内存。通过使用 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 方法。我也修改了我的代码来满足这一点。

© . All rights reserved.