C# 中的弱事件 — 如何避免令人讨厌的内存泄漏

作者:微信公众号:【架构师老卢】
6-5 17:43
136

概述:在 C# 中,事件和事件处理程序历来是对象之间通信的核心。在用户界面基于 WinForms 和 WPF 构建的应用程序中尤其如此。但是,误用事件会出现一个常见问题——内存泄漏!当事件订阅者未正确取消订阅时,就会发生这些泄漏,从而导致保留引用,从而无法释放内存。在 C# 中输入弱事件!我们将寻找一个有前途的解决方案来帮助应对这一挑战。弱事件允许垃圾回收器从订阅的对象中回收内存,而无需显式取消订阅操作,并有助于降低内存泄漏的风险。虽然我仍然鼓励您专注于应用程序中对象的适当生存期管理(毕竟这只是良好的实践和设计),但 C# 中的弱事件可以在这里为我们提供一些额外的保护!了解事件中的内存泄漏事件处理中

在 C# 中,事件和事件处理程序历来是对象之间通信的核心。在用户界面基于 WinForms 和 WPF 构建的应用程序中尤其如此。但是,误用事件会出现一个常见问题——内存泄漏!当事件订阅者未正确取消订阅时,就会发生这些泄漏,从而导致保留引用,从而无法释放内存。在 C# 中输入弱事件

我们将寻找一个有前途的解决方案来帮助应对这一挑战。弱事件允许垃圾回收器从订阅的对象中回收内存,而无需显式取消订阅操作,并有助于降低内存泄漏的风险。虽然我仍然鼓励您专注于应用程序中对象的适当生存期管理(毕竟这只是良好的实践和设计),但 C# 中的弱事件可以在这里为我们提供一些额外的保护!

了解事件中的内存泄漏

事件处理中的内存泄漏主要源于事件发布者和订阅者之间的持久引用。在典型方案中,发布者通过事件委托对订阅者具有强引用。如果订阅者在释放之前未能取消订阅,则 .NET 垃圾回收器无法回收订阅者的内存,因为发布者仍保留对它的引用。

这种情况在生存期较长的应用程序或动态创建和销毁许多对象的应用程序中尤其成问题。如果我们习惯于使用事件订阅创建对象,其中发布者和订阅者在应用程序的整个生命周期中都存在......没什么大不了的!如果我们动态地创建这些,并且生命周期管理很简单......也没什么大不了的!但是,在大型应用程序中,对象生存期管理可能会很快变得复杂。

C# 中的弱事件引入了一种机制,其中事件订阅不会阻止订阅者的垃圾回收,从而有效降低内存泄漏的风险。

什么是弱事件?

C# 中的弱事件是一种设计模式,它允许对事件订阅者进行垃圾回收,从而防止内存泄漏。传统上,这是不允许发生的,因为事件订阅使对象保持活动状态并防止它被收集 - 即使似乎没有其他人意识到它!

C# 中的弱事件在事件发布者的生命周期比订阅服务器长的情况下特别有用。通过在 C# 中实现弱事件,可以帮助确保订阅者不会仅仅因为订阅了某个事件而不必要地保留在内存中。这有助于保持最佳的内存使用率和应用程序性能...因为没有人愿意处理内存泄漏。尤其是在一种管理语言中,我们经常感到不受此影响!

WeakReference<T> 在弱事件中的作用

WeakReference<T>在 C# 中弱事件的实现中起着神奇的作用。它保存对对象(订阅者)的引用,而不会阻止该对象被垃圾回收。这意味着,如果没有对引用对象的强引用,垃圾回收器可以回收引用对象的内存。在弱事件的上下文中,允许对事件处理程序进行垃圾回收,这是防止具有复杂事件处理的应用程序中的内存泄漏的关键。

在 C 语言中实现弱事件#

在 C# 中实现弱事件涉及创建一个类来管理事件订阅 ,确保事件处理程序不会阻止订阅者的垃圾回收。以下是分步指南:WeakReference

  1. 定义 WeakEvent 类 — 创建一个类,该类包含指向事件处理程序的对象列表。WeakReference
  2. AddListener 方法 — 添加处理程序时,将其包装在 中,以确保它不会阻止订阅者的垃圾回收。此外,如果委托有目标,我们使用 a 来保持对委托的强引用,从而防止过早的垃圾回收,这对于匿名方法或 lambda 尤其重要。WeakReferenceConditionalWeakTable
  3. RemoveListener 方法 — 它涉及从侦听器列表中删除处理程序。此步骤对于允许垃圾回收器回收内存至关重要,尤其是在不再需要处理程序或其目标时。
  4. Raise 方法 — 这将遍历侦听器列表,调用那些仍然处于活动状态的侦听器。如果已收集目标,则会从列表中删除侦听器,从而确保类不会保留死引用。

代码片段:WeakEvent

让我们看一下如何在 C# 中执行弱事件的示例实现:

public sealed class WeakEvent<TEventArgs>
    where TEventArgs : EventArgs
{
    private readonly List<WeakReference<EventHandler<TEventArgs>>> _handlers;
    private readonly ConditionalWeakTable<object, List<object>> _keepAlive;

    public WeakEvent()
    {
        _handlers = [];
        _keepAlive = [];
    }

    public void Subscribe(EventHandler<TEventArgs>? handler)
    {
        if (handler == null)
        {
            return;
        }

        _handlers.Add(new(handler));

        if (handler.Target != null)
        {
            var delegateList = _keepAlive.GetOrCreateValue(handler.Target);
            delegateList.Add(handler);
        }
    }

    public void Ubsubscribe(EventHandler<TEventArgs>? handler)
    {
        if (handler == null)
        {
            return;
        }

        _handlers.RemoveAll(wr =>
            wr.TryGetTarget(out var existingHandler) &&
            existingHandler == handler);

        if (handler.Target != null &&
            _keepAlive.TryGetValue(handler.Target, out var delegateList))
        {
            delegateList.Remove(handler);
        }
    }

    public void Raise(object sender, TEventArgs e)
    {
        foreach (var weakReference in _handlers.ToList())
        {
            if (weakReference.TryGetTarget(out var handler))
            {
                handler(sender, e);
            }
            else
            {
                _handlers.Remove(weakReference);
            }
        }
    }
}

上面的示例提供了将事件处理程序挂钩和取消挂钩到源弱事件的机制。我们还可以调用以触发事件,并支持使用我们的变量处理匿名委托。这有助于支持匿名代表在不应该收集垃圾时收集垃圾!

弱事件的用法示例

下面是一个示例,说明我们如何利用带有 and 的类:WeakEvent<T>EventSourceEventListener

var eventSource = new EventSource();
var listener = new EventListener();
eventSource.CustomEvent.Subscribe(listener.OnCustomEvent);

eventSource.TriggerEvent();
eventSource.CustomEvent.Unsubscribe(listener.OnCustomEvent);

Console.ReadLine();

public sealed class EventSource
{
    private readonly WeakEvent<EventArgs> _customEvent;

    public EventSource()
    {
         _customEvent = new WeakEvent<EventArgs>();
    }

    public void Subscribe(EventHandler<TEventArgs>? handler) =>
        _customEvent.Subscribe(handler);

    public void Unsubscribe(EventHandler<TEventArgs>? handler) =>
        _customEvent.Unsubscribe(handler);

    public void TriggerEvent() =>
        _customEvent .Raise(this, EventArgs.Empty);
}

public sealed class EventListener
{
    public void OnCustomEvent(object? sender, EventArgs e)
    {
        Console.WriteLine("Event received.");
    }
}

在 C# 中使用弱事件的最佳实践

现在我们已经了解了如何在 C# 中使用弱事件(以及原因),让我们讨论一些最佳实践:

  • 了解何时使用它们:在事件发布者寿命超过订阅者的方案中,弱事件最有用,例如在具有多个临时视图的 UI 应用程序中,或者在事件订阅动态更改的长时间运行的应用程序中。
  • 正确实现:确保使用 OR 正确实现弱事件,以防止内存泄漏,同时仍允许垃圾回收。WeakReferenceConditionalWeakTable
  • 测试和调试:定期测试应用程序是否存在与事件处理相关的内存泄漏和性能问题。Visual Studio 诊断工具等工具可以帮助识别泄漏。Visual Studio 2022 17.9 还支持事件处理程序泄漏见解
  • 平衡使用:虽然弱事件可以缓解内存泄漏,但它们会带来复杂性。在必要时使用它们,但在简单的事件处理就足够了的情况下避免过度使用。

C# 中的弱事件是有效管理事件处理程序内存的有用解决方案。在对象生存期不断变化的复杂或动态应用中尤其如此。通过允许对订阅者进行垃圾回收,弱事件可以防止潜在的内存泄漏,从而提高应用程序性能。正确使用它们,以及了解它们最适合的场景,可以帮助您避免事件出现内存问题!

阅读排行