当前位置:首页 > Windows程序 > 正文

The .NET weak event pattern in C#

2021-03-26 Windows程序

As you may know event handlers are a common source of memory leaks caused by the persistence of objects that are not used anymore, and you may think should have been collected, but are not, and for good reason.

In this (hopefully) short article, I’ll present the issue with event handlers in the context of the .Net framework, then I’ll show you how you can implement the standard solution to this issue, the weak event pattern, in two ways, either using:

the “legacy” (well, before .Net 4.5, so not that old) approach which is quite cumbersome to implement

the new approach provided by the .Net 4.5 framework which is as simple as it can be

(The source code is available here.)

The common stuff

Before diving into the core of the article let’s review two items which are used extensively in the code: a class and a method.

The event-source

Let me present you a basic but useful event-source class which exposes just enough complexity to illustrate the point:

Hide   Copy Code

public class EventSource { public event EventHandler<EventArgs> Event = delegate { }; public void Raise() { Event(this, EventArgs.Empty); } }

For those who wonder what the strange empty delegate initialization is: it’s a trick to be sure the event is always initialized, without having to check each time if it’s non-null before using it.

The GC triggering utility method

In .Net the garbage collection is triggered in a non-deterministic manner, which is not good for our tests that need to track the state of objects in a deterministic manner.

So we’ll regularly have to trigger ourselves a GC, and to avoid duplicating plumbing code it’s been factorized in a dedicated method:

Hide   Copy Code

static void TriggerGC() { Console.WriteLine("Starting GC."); GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect(); Console.WriteLine("GC finished."); }

Not rocket science but it deserves a little explanation if you’re not familiar with this pattern:

first GC.Collect() triggers the .Net CLR garbage-collector which will take care of sweeping unused objects, and for objects whose class has no finalizer (a.k.a destructor in C#) it’s enough

GC.WaitForPendingFinalizers() waits for the finalizers of other objects to execute; we need it because as you’ll see we’ll use the finalizer methods to know when our objects are collected

second GC.Collect() ensures the newly finalized objects are swept too.

The issue

So first thing first, let’s try to understand what’s the problem with event listeners, with the help of some theory and, most importantly, a demo.

Background

When an object acting as an event listener registers one of its instance methods as an event handler on an object that produces events (the event source), the event source must keep a reference to the event listener object in order to raise the event in the context of this listener.
This is fair enough, but if this reference is a strong reference then the listener acts as a dependency of the event source and can’t be garbage-collected even if the last object referencing it is the event source.

Here is a detailed diagram of what happens under the hood:

技术分享

Events handlers issue

This is not an issue if you can control the life time of the listener object as you can unsubscribe from the event source when you don’t need the listener anymore, typically using the disposable pattern.
But if you can’t identify a single point of responsibility for the life time of the listener then you can’t dispose of it in a deterministic manner and you have to rely on the garbage collection process … which will never consider your object as ready for collection as long as the event source is alive!

Demo

Theory is all good but let’s see the issue with real code.

Here is our brave event listener, just a bit naive, and we’ll quickly understand why:

Hide   Copy Code

public class NaiveEventListener { private void OnEvent(object source, EventArgs args) { Console.WriteLine("EventListener received event."); } public NaiveEventListener(EventSource source) { source.Event += OnEvent; } ~NaiveEventListener() { Console.WriteLine("NaiveEventListener finalized."); } }

Let’s see how this implementation behaves with a simple use-case:

温馨提示: 本文由Jm博客推荐,转载请保留链接: https://www.jmwww.net/file/68145.html