This project has moved. For the latest updates, please go here.

Nicenis has moved to GitHub: https://github.com/Nicenis/Nicenis

Introduction

Attaching an event handler can cause memory leaks if event publisher lives longer than event listener. For example, this is a class named EventSource that defines an event named EventRaised:

public partial class EventSource
{
    public event EventHandler EventRaised;

    protected virtual void OnEventRaised()
    {
        var eventRaised = EventRaised;
        if (eventRaised != null)
            eventRaised(sender: this, e: EventArgs.Empty);
    }
}

The class has a method named Raise to raise the EventRaised event:

public partial class EventSource
{
    // Raises a EventRaised event.
    public void Raise()
    {
        OnEventRaised();
    }
}

And this is a class named Listener that attaches an event handler to the EventRaised event:

public class Listener
{
    public Listener(EventSource source)
    {
        // Attaches an event handler in the constructor.
        source.EventRaised += Source_EventRaised;
    }

    private void Source_EventRaised(object sender, EventArgs e)
    {
        Console.WriteLine("Source_EventRaised");
    }
}

And this is a test code:

class Program
{
    static void Main(string[] args)
    {
        // Creates an event source.
        var source = new EventSource();

        // Creates a listener.
        var listener = new Listener(source);

        // Raises a EventRaised event.
        source.Raise();

        // Clears the listener instance reference.
        listener = null;

        // Forces garbage collection.
        GC.Collect();

        // Raises a EventRaised event.
        source.Raise();
    }
}

The above code clears the listener instance reference and forces garbage collection. If the listener instance is properly garbage collected, the last Raise method call does nothing. But if you run the test code, the result will look like this:

Source_EventRaised
Source_EventRaised

The listener instance is not garbage collected because the EventRaised event handler holds the listener instance reference.

Using the WeakEventHandler

The EventRaised event can be rewritten with the WeakEventHandler like this to avoid the reference problem:

public partial class EventSource
{
    readonly WeakEventHandler _eventRaised = new WeakEventHandler();

    public event EventHandler EventRaised
    {
        add { _eventRaised += value; }
        remove { _eventRaised -= value; }
    }

    protected virtual void OnEventRaised()
    {
        _eventRaised.Invoke(sender: this);
    }
}

The WeakEventHandler uses the WeakReference to save instance references. So it allows GC to collect instances. If you run the test code with the above EventSource class, the result will look like this:

Source_EventRaised

If a custom event arguments class is required, you can use the WeakEventHandler like this:

public partial class EventSource
{
    public class EventRaised2EventArgs : EventArgs
    {
        public int Data { get; set; }
    }

    readonly WeakEventHandler<EventRaised2EventArgs> _eventRaised2
                                    = new WeakEventHandler<EventRaised2EventArgs>();

    public event EventHandler<EventRaised2EventArgs> EventRaised2
    {
        add { _eventRaised2 += value; }
        remove { _eventRaised2 -= value; }
    }

    protected virtual void OnEventRaised2(EventRaised2EventArgs e)
    {
        _eventRaised2.Invoke(sender: this, e: e);
    }
}

Using with Lambdas

If you create a lamda expression that does not access any instance member, it is treated as a static method. So if you attach it to an event, it means that a static method is attached. For example, the following class attaches a static lambda expression.

public class ListenerWithStaticLambda
{
    public ListenerWithStaticLambda(EventSource source)
    {
        // Attaches a lambda expression that does not access any instance member.
        source.EventRaised += (_, __) =>
        {
            Console.WriteLine("Source_EventRaised");
        };
    }
}

The attached static lambda is not automatically detached when its host instance is garbage collected because it is not related to the host instance. So if you run the following test code:

class Program
{
    static void Main(string[] args)
    {
        // Creates an event source.
        var source = new EventSource();

        // Creates a listener that attaches a static lambda expression.
        var listener = new ListenerWithStaticLambda(source);

        // Raises a EventRaised event.
        source.Raise();

        // Clears the listener instance reference.
        listener = null;

        // Forces garbage collection.
        GC.Collect();

        // Raises a EventRaised event.
        source.Raise();
    }
}

The result will look like this:

Source_EventRaised
Source_EventRaised

If you want to avoid the above problem, you must create a lambda expression that acceses one or more instance members. The following is an example:

public class ListenerWithLambda
{
    int _field = 0;

    public ListenerWithLambda(EventSource source)
    {
        // Attaches a lambda expression that accesses an instance member.
        source.EventRaised += (_, __) =>
        {
            _field++;
            Console.WriteLine("Source_EventRaised");
        };
    }
}

Threading

The WeakEventHandler is not thread-safe. So if it is accessed by multiple threads, you must protect it. The following is an example:

public partial class EventSource
{
    readonly WeakEventHandler _eventRaised = new WeakEventHandler();

    public event EventHandler EventRaised
    {
        add
        {
            lock (_eventRaised)
                _eventRaised += value;
        }
        remove
        {
            lock (_eventRaised)
                _eventRaised -= value;
        }
    }

    protected virtual void OnEventRaised()
    {
        lock (_eventRaised)
            _eventRaised.Invoke(sender: this);
    }
}

Last edited Dec 31, 2016 at 8:16 PM by Ryeol, version 11

Comments

No comments yet.