Search code examples
c#garbage-collectionweak-references

Why is this weak reference to event handler garbage collected?


Why is garbage-collected event handler in following example?
I would expect event received after garbage collection, but it is not.
Question is not about WeakEventManager.

class WeakEventTest
{
    public static void Run() {
        EventConsumer ec = new EventConsumer();
        WeakEvent<EventArgs> weakEvent = new WeakEvent<EventArgs>();
        EventHandler<EventArgs> eh = ec.HandleEvent;
        weakEvent += new WeakReference<EventHandler<EventArgs>>(ec.HandleEvent);

        Console.WriteLine("Calling trigger");
        weakEvent.Trigger(null, EventArgs.Empty);

        Console.WriteLine("Calling System.GC.Collect");
        GC.Collect();
        GC.WaitForPendingFinalizers();
        GC.WaitForFullGCComplete();
        GC.Collect();

        // event handler not called here
        Console.WriteLine("Calling trigger");
        weakEvent.Trigger(null, EventArgs.Empty);
    }


}

class EventConsumer
{
    public void HandleEvent(object obj, EventArgs args)
    {
        Console.WriteLine("EventReceived");
    }
}

public class WeakEvent<T>
{
    private List<WeakReference<EventHandler<T>>> referenceList = new List<WeakReference<EventHandler<T>>>();
    private EventHandler<T> handler = null;

    public static WeakEvent<T> operator +(WeakEvent<T> a, EventHandler<T> b)
    {
        lock (a.referenceList)
        {
            a.handler += b;
        }
        return a;
    }

    public static WeakEvent<T> operator +(WeakEvent<T> a, WeakReference<EventHandler<T>> b)
    {
        lock (a.referenceList)
        {
            a.referenceList.Add(b);
        }
        return a;
    }

    public static WeakEvent<T> operator -(WeakEvent<T> a, EventHandler<T> b)
    {
        lock (a.referenceList)
        {
            for (int i = a.referenceList.Count - 1; i >= 0; i--)
            {
                WeakReference<EventHandler<T>> wr = a.referenceList[i];
                EventHandler<T> target;
                if (!wr.TryGetTarget(out target))
                {
                    a.referenceList.RemoveAt(i);
                    continue;
                }
                if (Object.ReferenceEquals(target, b))
                {
                    a.referenceList.RemoveAt(i);
                    break;
                }
            }
            a.handler -= b;
        }
        return a;
    }

    public void Trigger(object obj, T args)
    {
        lock (referenceList)
        {
            for (int i = referenceList.Count - 1; i >= 0; i--)
            {
                WeakReference<EventHandler<T>> wr = referenceList[i];
                EventHandler<T> target;
                if (!wr.TryGetTarget(out target))
                {
                    referenceList.RemoveAt(i);
                    continue;
                }
                target(obj, args);
            }

            if (handler != null)
            {
                handler(obj, args);
            }
        }
    }

    public WeakEvent<T> AddWeakHandler(EventHandler<T> b)
    {
        lock (referenceList)
        {
            referenceList.Add(new WeakReference<EventHandler<T>>(b));
        }
        return this;
    }

Output in console is:
Calling trigger
EventReceived
Calling System.GC.Collect
Calling trigger
--> here I would expect EventReceived

In following simple example reference is not garbage collected and works as expected.

class Program
{
    static void Main(string[] args)
    {
        var ec = new EventConsumer();
        var wr = new WeakReference<EventHandler<EventArgs>>(ec.EventReceived);

        EventHandler<EventArgs> target;
        if (wr.TryGetTarget(out target))
        {
            Console.WriteLine("Raising event");
            target(null, EventArgs.Empty);
        }

        Console.WriteLine("Calling System.GC.Collect");
        GC.Collect();
        GC.WaitForPendingFinalizers();
        GC.WaitForFullGCComplete();
        GC.Collect();

        EventHandler<EventArgs> target2;
        if (wr.TryGetTarget(out target2))
        {
            Console.WriteLine("Raising event");
            target2(null, EventArgs.Empty);
        }

        Console.ReadKey();
    }
}

public class EventConsumer
{
    public void EventReceived(object obj, EventArgs args)
    {
        Console.WriteLine("EventReceived");
    }
}

Solution

  • The first one is the expected result: you're referencing ec only using weak references, so it has no reason not to be collected.

    The second example is more subtle: ec is kept alive because you're keeping a reference on target (which in turn references ec). Just clear that reference and you'll observe the same behavior as the first example:

    class Program
    {
        static void Main(string[] args)
        {
            var ec = new EventConsumer();
            var wr = new WeakReference<EventHandler<EventArgs>>(ec.EventReceived);
    
            EventHandler<EventArgs> target;
            if (wr.TryGetTarget(out target))
            {
                Console.WriteLine("Raising event");
                target(null, EventArgs.Empty);
            }
    
            // Clear the reference
            target = null;
    
            Console.WriteLine("Calling System.GC.Collect");
            GC.Collect();
            GC.WaitForPendingFinalizers();
            GC.WaitForFullGCComplete();
            GC.Collect();
    
            EventHandler<EventArgs> target2;
            if (wr.TryGetTarget(out target2))
            {
                Console.WriteLine("Raising event");
                target2(null, EventArgs.Empty);
            }
    
            Console.ReadKey();
        }
    }
    
    public class EventConsumer
    {
        public void EventReceived(object obj, EventArgs args)
        {
            Console.WriteLine("EventReceived");
        }
    }
    

    Note that you need to compile in release mode. In debug mode, objects are kept alive until the end of the method (even if they're not referenced anymore) to make debugging easier.