0

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");
    }
}
Martin Samek
  • 173
  • 7
  • That's what "weak" means. Your second snippet isn't quite weak enough when you [use a debugger](http://stackoverflow.com/questions/17130382/understanding-garbage-collection-in-net/17131389#17131389). Get it to behave the same way by running the code the way it will on your user's machine. Switch to the Release build and use Tools > Options > Debugging > General > untick "Suppress JIT optimization". – Hans Passant Aug 03 '16 at 12:55

1 Answers1

1

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.

Kevin Gosse
  • 38,392
  • 3
  • 78
  • 94
  • I was missing that ec.EventReceived is something like pointer to function and I had only weak reference to ec.EventReceived. The function was still existing (I had strong reference to ec), but the pointer was garbage-collected. Am I right? – Martin Samek Aug 03 '16 at 13:19