From translator
Recently, in a project where I work, we are faced with a memory leak problem. After reading a lot of articles, from stories on memory management in .NET to practical recommendations on the correct release of resources, I also came across an article that tells how to use events correctly. I want to present her translation.
This is a topic from the sandbox with which I got here on Habr.
Introduction
When using ordinary events in C #, subscribing to an event creates a link from the object containing the event to the subscriber object.

')
If the source object lives longer than the subscriber object, memory leaks are possible: in the absence of other references to the subscriber, the source object will still refer to it. Therefore, the memory that the subscriber occupies cannot be freed by the garbage collector.
There are many approaches to solve this problem. In this article some of them will be considered, the advantages and disadvantages are discussed. I divided all approaches into two parts: in one, we will assume that the source of the event is an already existing class with a regular event; in the other, we will change the original object itself to look at the work of various methods.
What are the events?
Many developers think that events are a list of delegates. This is not true. As you know, delegates can be multicast - contain links to several functions at once:
EventHandler eh = Method1; eh += Method2;
What then are the events? They are similar to properties: inside they contain a delegate field, access to which is denied directly. A delegate's public field (or public property) can cause the list of event handlers to be cleared by another object, or that the event will be triggered externally — while we only want to call it from the original object.
Properties are a pair of get / set methods. Events are a pair of add / remove methods.
public event EventHandler MyEvent { add { ... } remove { ... } }
Only methods for adding and removing handlers should be public. In this case, other classes will not be able to get a list of handlers, will not be able to clear it, or trigger an event.
Sometimes the short syntax of declaring events in C # is misleading:
public event EventHandler MyEvent;
In fact, this compilation of the record takes place in:
private EventHandler _MyEvent; // public event EventHandler MyEvent { add { lock(this) { _MyEvent += value; } } remove { lock(this) { _MyEvent -= value; } } }
In C #, events are implemented by default using synchronization, using for it objects in which they are declared. You can verify this with the help of the disassembler - the add and remove methods are marked with the attribute [MethodImpl (MethodImplOptions.Synchronized)], which is equivalent to synchronization using the current instance of the object.
Subscribing and unsubscribing from an event is a thread safe operation. However, the thread-safe event call is left to the discretion of the developers, and quite often they do it incorrectly:
if (MyEvent != null) MyEvent(this, EventArgs.Empty);
Another common option is to preserve the delegate in a local variable.
EventHandler eh = MyEvent; if (eh != null) eh(this, EventArgs.Empty);
Is this code thread safe? It depends. According to the memory model described in the C # language specification, this example is not thread-safe: the JIT compiler, by optimizing the code, can delete local variables. However, the .NET runtime (starting with version 2.0) has a stronger memory model, and in it this code is thread-safe.
The correct solution, according to the ECMA specification, is to assign a local variable in the lock (this) block or use a volatile-field to store the reference to the delegate.
EventHandler eh; lock (this) { eh = MyEvent; } if (eh != null) eh(this, EventArgs.Empty);
Part 1: Weak events on the subscriber side
In this part, we will assume that we have a normal event (with reference to handlers), and any unsubscription of it should be made on the side of subscribers.
Solution 0: Just unsubscribe
void RegisterEvent() { eventSource.Event += OnEvent; } void DeregisterEvent() { eventSource.Event -= OnEvent; } void OnEvent(object sender, EventArgs e) { ... }
Simple and effective, something you should use whenever possible. However, it is not always possible to provide a call to the DeregisterEvent method after the object is no longer used. You can try to use the Dispose method, although it is usually used for unmanaged resources. The finalizer will not work in this case: the garbage collector will not call it, because the source object refers to our subscriber!
BenefitsEasy to use if using an object implies calling Dispose.
disadvantagesExplicit memory management is complex. The Dispose method may also be forgotten.
Solution 1: Unsubscribe from the event after calling it.
void RegisterEvent() { eventSource.Event += OnEvent; } void OnEvent(object sender, EventArgs e) { if (!InUse) { eventSource.Event -= OnEvent; return; } ... }
Now we don’t need to worry if anyone will tell us that the subscriber object is no longer in use. We ourselves check this after calling the event. However, if we cannot use solution 0, then, as a rule, it is impossible to determine from the object itself whether it is used. And given the fact that you are reading this article, you have probably come across one of these cases.
It should be noted that this solution is already losing to decision 0: if the event is not triggered, then we will get a memory leak taken by the subscriber. Imagine that many objects subscribed to the SettingsChanged static event. Then all these objects will not be removed by the garbage collector until the event works - and this may never happen.
BenefitsNot.
disadvantagesMemory leak if event is not triggered. It is also difficult to determine if the object is in use.
Solution 2: Weak Link Wrapper
This solution is almost identical to the previous one, except that we place the code of the event handler in a wrapper class, which then redirects the call to a subscriber object accessible via a
weak reference . Using a weak link, you can easily check whether a subscriber object still exists.

EventWrapper ew; void RegisterEvent() { ew = new EventWrapper(eventSource, this); } void OnEvent(object sender, EventArgs e) { ... } sealed class EventWrapper { SourceObject eventSource; WeakReference wr; public EventWrapper(SourceObject eventSource, ListenerObject obj) { this.eventSource = eventSource; this.wr = new WeakReference(obj); eventSource.Event += OnEvent; } void OnEvent(object sender, EventArgs e) { ListenerObject obj = (ListenerObject)wr.Target; if (obj != null) obj.OnEvent(sender, e); else Deregister(); } public void Deregister() { eventSource.Event -= OnEvent; } }
BenefitsAllows the garbage collector to free the memory occupied by the subscriber.
disadvantagesA leak of memory taken up by the wrapper if the event never works. Writing wrapper classes for each event is a bunch of duplicate code.
Solution 3: Unsubscribe from the event in the finalizer
In the previous example, we stored a reference to the EventWrapper and had a public method Deregister. We can add a finalizer (destructor) to the subscriber and use it to unsubscribe from the event.
~ListenerObject() { ew.Deregister(); }
This method will save us from memory leaks, but you have to pay for it: the garbage collector spends more time removing objects with finalizers. When a subscriber object stops referring to anyone (except for weak links), it will outlive the first garbage collection and be transferred to a higher generation. Then the garbage collector will call the finalizer, and only after that the object can be removed at the next garbage collection (already in the new generation).
It should also be noted that finalizers are called in a separate thread. This can cause an error if the subscription / unsubscribe event is implemented in a non-thread safe manner. Remember that the default implementation of events in C # is not thread-safe!
BenefitsAllows the garbage collector to free the memory occupied by the subscriber. There is no memory leak taken by the wrapper.
disadvantagesHaving a finalizer increases the time that an unused object will be in memory before it is deleted. A thread-safe event implementation is required. Lots of duplicate code.
Solution 4: Reuse wrapper
The code below contains a wrapper class that can be reused. Using lambda expressions, we pass a different code: to subscribe to an event, unsubscribe from it, and to transfer the event to a private method.
eventWrapper = WeakEventHandler.Register( eventSource, (s, eh) => s.Event += eh, // (s, eh) => s.Event -= eh, // this, // (me, sender, args) => me.OnEvent(sender, args) // );

The returned eventWrapper instance has only one public method - Deregister. We need to be careful when writing lambda expressions: since they are compiled into delegates, they can also contain references to objects. That is why the subscriber returns as me. If we wrote (me, sender, args) => this.OnEvent (sender, args), then the lambda expression would be attached to the this variable, thereby triggering the creation of a closure. And since the WeakEventHandler contains a reference to the delegate causing the event, this would result in a “strong” (normal) link from the wrapper to the subscriber. Fortunately, we have the opportunity to check whether the delegate has captured any variables: for such lambda expressions, the compiler will create instance methods; otherwise, the methods will be static. WeakEventHandler checks for this using the Delegate.Method.IsStatic flag and throws an exception if the lambda expression was not written correctly.
This approach allows reuse of the wrapper, but still requires its own wrapper class for each delegate type. Since you can actively use System.EventHandler and System.EventHandler, if there are many different types of delegates, you will want to automate all of this. To do this, you can use code generation or the System.Reflection.Emit space types.
BenefitsAllows the garbage collector to free the memory occupied by the subscriber. Not a very large amount of additional code.
disadvantagesLeakage of memory occupied by the wrapper in case the event never works.
Solution 5: WeakEventManager
WPF has built-in support for subscriber-side weak events through the WeakEventManager class. It works like previous solutions using wrappers, except that a single instance of WeakEventManager serves as a wrapper between multiple event sources and multiple subscribers. Due to the fact that there is only one instance of the object, WeakEventManager avoids memory leaks even if the event is not triggered: subscribing to another event can clear the list of old subscriptions. These cleanups are done by the WPF dispatcher in threads that are running the WPF message loop.
WeakEventManager also has an additional limitation: it requires the sender parameter to be set correctly. If you use it for the button.Click event, then only events with sender == button will be passed to subscribers. Some event implementations may attach handlers to other events:
public event EventHandler Event { add { anotherObject.Event += value; } remove { anotherObject.Event -= value; } }
Such events cannot be used in WeakEventManager.
One WeakEventManager per event, one instance per stream. A recommended template for defining such events with code blanks can be found in the
WeakEvent Templates article in MSDN.
Fortunately, we can simplify this with the help of Generics:
public sealed class ButtonClickEventManager : WeakEventManagerBase<ButtonClickEventManager, Button> { protected override void StartListening(Button source) { source.Click += DeliverEvent; } protected override void StopListening(Button source) { source.Click -= DeliverEvent; } }
Notice that DeliverEvent takes as arguments (object, EventArgs), while the Click event provides arguments (object, RoutedEventArgs). In C # there is no support for converting between types of delegates, however there is support for
contra parsing when creating delegates from a group of methods .
BenefitsAllows the garbage collector to free the memory occupied by the subscriber. The memory occupied by the wrapper can also be freed.
disadvantagesThe method is not quite suitable for applications where there is no graphical interface, because the implementation is bound to WPF.
Part 2: Weak events on the source side
In this section, we will look at how to implement weak events by modifying the original object containing the event. All solutions proposed below have an advantage over the implementation of weak events on the subscriber side: we can easily make subscription / unsubscribe thread-safe.
Solution 0: Interface
WeakEventManager is worth mentioning in this part. As a wrapper, it joins regular events (the subscriber side), but it can also provide weak events to clients (the original object side).
There is an IWeakEventListener interface. Subscribers implementing this interface will be referenced by the source object via a weak reference, and called the implemented method ReceiveWeakEvent.
BenefitsSimple and effective.
disadvantagesWhen an object is subscribed to a multitude of events, in the implementation of ReceiveWeakEvent you will have to write a bunch of checks on the event and source types.
Solution 1: Weak delegate reference
This is another approach used in WPF: CommandManager.InvalidateRequery looks like a normal event, but it is not. It contains a weak reference to the delegate, so subscribing to a static event does not result in a memory leak.

This is a simple solution, but event subscribers can easily forget about it or misunderstand:
CommandManager.InvalidateRequery += OnInvalidateRequery;
The problem is that the CommandManager contains only a weak reference to the delegate, and the subscriber does not contain any references to the delegate at all. Therefore, during the next garbage collection, the delegate will be deleted and OnInvalidateRequery will no longer work, even if the subscriber object is still in use. For the fact that the delegate will live in memory, the subscriber must be responsible.

class Listener { EventHandler strongReferenceToDelegate; public void RegisterForEvent() { strongReferenceToDelegate = new EventHandler(OnInvalidateRequery); CommandManager.InvalidateRequery += strongReferenceToDelegate; } void OnInvalidateRequery(...) {...} }
BenefitsThe memory occupied by delegates is freed.
disadvantagesIf you forget to put a “strong” reference to the delegate, then the event will be triggered before the first garbage collection. This can be difficult to find errors.
Solution 2: Object + Forwarder
While WeakEventManager was adapted for solution 0, the WeakEventHandler wrapper is adapted to this solution: registering the <object, ForwarderDelegate> pair.

eventSource.AddHandler(this, (me, sender, args) => ((ListenerObject)me).OnEvent(sender, args));
BenefitsSimple and effective.
disadvantagesUnusual way to register events; The redirect lambda expression requires type conversion.
Solution 3: SmartWeakEvent
The SmartWeakEvent shown below provides an event that looks like a normal .NET event, but keeps a weak link to the subscriber. So there is no need to keep a “strong” reference to the delegate.
void RegisterEvent() { eventSource.Event += OnEvent; } void OnEvent(object sender, EventArgs e) { ... }
We define the event:
SmartWeakEvent<EventHandler> _event = new SmartWeakEvent<EventHandler>(); public event EventHandler Event { add { _event.Add(value); } remove { _event.Remove(value); } } public void RaiseEvent() { _event.Raise(this, EventArgs.Empty); }
How it works? Using the properties Delegate.Target and Delegate.Method, each delegate is divided into a target object (stored using a weak reference) and MethodInfo. When an event is triggered, the method is invoked using reflection.

The vulnerability of this method is that someone can attach an anonymous method as an event handler.
int localVariable = 42; eventSource.Event += delegate { Console.WriteLine(localVariable); };
In this case, the target object is a closure that can be immediately removed by the collector, since there are no links to it. However, SmartWeakEvent can recognize such cases and throws an exception, so you should not have any problems debugging, because the event handler is decoupling earlier than you think.
if (d.Method.DeclaringType.GetCustomAttributes(typeof(CompilerGeneratedAttribute), false).Length != 0) throw new ArgumentException(...)
BenefitsIt looks really like a weak event; There is practically no redundant code.
disadvantagesThe implementation of methods through reflection is rather slow. It does not work with partial permission, since it performs private methods.
Solution 4: FastSmartWeakEvent
The functionality and usage is similar to the solution with SmartWeakEvent, but the performance is much higher.
Here are the test results for an event with two delegates (one refers to an instance method, the other to a static one):
Normal ("strong") event ... 16 948 785 calls per second
Smart weak event… 91,960 calls per second
Fast smart weak event ... 4 901 840 calls per second
How it works? We no longer use reflection to perform the method. Instead, we compile the method (similar to the method in the previous solution) during program execution using System.Reflection.Emit.DynamicMethod.
BenefitsLooks like a real weak event; There is practically no redundant code.
disadvantagesIt does not work with partial permission, since it performs private methods.
suggestions
- Use WeakEventManager for everything that runs in the GUI thread in WPF applications (for example, for user controls that subscribe to events of model objects)
- Use FastSmartWeakEvent if you want to provide a weak event.
- Use WeakEventHandler if you want to subscribe to an event.