(1 item) |
|
(1 item) |
|
(5 items) |
|
(1 item) |
|
(1 item) |
|
(2 items) |
|
(2 items) |
|
(4 items) |
|
(1 item) |
|
(6 items) |
|
(2 items) |
|
(4 items) |
|
(1 item) |
|
(4 items) |
|
(2 items) |
|
(1 item) |
|
(1 item) |
|
(1 item) |
|
(1 item) |
|
(1 item) |
|
(1 item) |
|
(1 item) |
|
(1 item) |
|
(2 items) |
|
(2 items) |
|
(5 items) |
|
(3 items) |
|
(1 item) |
|
(1 item) |
|
(1 item) |
|
(3 items) |
|
(1 item) |
|
(1 item) |
|
(2 items) |
|
(8 items) |
|
(2 items) |
|
(7 items) |
|
(2 items) |
|
(2 items) |
|
(1 item) |
|
(2 items) |
|
(1 item) |
|
(2 items) |
|
(4 items) |
|
(1 item) |
|
(5 items) |
|
(1 item) |
|
(3 items) |
|
(2 items) |
|
(2 items) |
|
(8 items) |
|
(7 items) |
|
(3 items) |
|
(7 items) |
|
(6 items) |
|
(1 item) |
|
(2 items) |
|
(5 items) |
|
(5 items) |
|
(7 items) |
|
(3 items) |
|
(7 items) |
|
(16 items) |
|
(10 items) |
|
(27 items) |
|
(15 items) |
|
(15 items) |
|
(13 items) |
|
(16 items) |
|
(15 items) |
Philip Haack recently wrote about the fact that in .NET, delegates hold a reference to their target object, and can therefore cause the target to be kept alive. Once you've understood how delegates work, this is pretty straightforward, and unsurprising. The reason that people are often tripped up by this seems to be that they regard delegates as being opaque magic things, rather than thinking about how they work.
It's actually easy enough to build your own delegate-like object, which can help offer some insight into how the
real thing behaves. Here's a home-made version of the standard System.EventHandler
delegate type:
public class MyEventHandler { private object target; private MethodInfo method; public MyEventHandler(object target, MethodInfo method) { this.target = target; this.method = method; } public void Invoke(object sender, EventArgs e) { object[] args = { sender, e }; method.Invoke(target, args); } }
It's missing a few frills, but this does provides the same core functionality as the standard EventHandler
- it's
capable of referring to a method with a particular signature on any object you like, and provides a means of invoking that
method. Obviously it's missing a few features - it doesn't do multicast or asynchronous invocation. Also, it doesn't offer the
compile-time type checking that C# does for real delegates. But the basic functionality is there, and with this example, it's
pretty clear that this holds a reference to the object that it invokes methods on. So it seems equally clear that a real delegate
will do much the same thing. (Although as mentioned
previously, it is possible to use weak
references to get away from this strong reference behaviour if you don't like it.)
This has implications for garbage collection: as long as something keeps a delegate reachable, that delegate will in turn keep its target reachable.
Because this isn't entirely obvious if you have never thought all that hard about what a delegate does, it often surprises people - not everyone expects objects that handle events to be kept alive for at least as long as the event source itself stays alive. This in turn gives rise to a popular misconception.
In the comments of Philip Haack's article, Brad Wilson propagates the standard myth:
"Yes, delegates are a hard reference. Yes, your object will stay alive. In fact, this can cause of some circular reference memory leaks."
To be fair, the first two sentences are correct. It's the third one that is completely misleading - the leaks that you can cause in .NET by not understanding this issue have absolutely nothing to do with circular references.
Circular references don't cause objects to be kept alive. This is easy to demonstrate:
using System; class HoldRef { public object target; private string n; public HoldRef(string name) { n = name; } ~HoldRef() { Console.WriteLine("GC finalizing " + n); } } class App { static void Main() { HoldRef first = new HoldRef("first"); HoldRef second = new HoldRef("second"); // Set up a circular reference first.target = second; second.target = first; // Let go of root references first = null; second = null; // GC GC.Collect(); GC.WaitForPendingFinalizers(); Console.WriteLine("Done GC"); Console.ReadLine(); } }
This creates two objects that refer to each other, so we have a circular reference. It then removes any root references that were referring to the objects, and forces a garbage collect. I get this output:
GC finalizing second GC finalizing first Done GC
So as you can see, the presence of a circular reference is not sufficient to cause a leak in a garbage collected environment. (In a reference counting system like COM, circular references do cause leaks of course. This one of the main reasons .NET uses GC in the first place.)
It's only slightly more effort to prove that in the more specific case of a circular reference between a Form and an event handler, the circular reference still doesn't cause a leak. I wrote the following code:
public class Handler { private static int lastID; public readonly int ID; private Form theTarget; public static Handler AttachWithCircularReference(Form f) { Handler h = new Handler(f); h.theTarget = f; return h; } public static Handler AttachWithUnidirectionalReference(Form f) { return new Handler(f); } private Handler(Form f) { ID = lastID++; f.Click += new EventHandler(f_Click); f.Text = ID.ToString(); } private void f_Click(object sender, EventArgs e) { MessageBox.Show("Clicked", ID.ToString()); } ~Handler() { MessageBox.Show("Handler GCed ", ID.ToString()); } }
Instances of this Handler
class attach to the Click
event of a Form
.
This will mean that the Form
has a reference to a delegate holding a reference to the Handler
.
This means that if the Form
is reachable (and therefore not collectable by the GC), the
attached Handler
will also remain reachable. Also, the Handler
has a field,
theTarget
, which can optionally point back to the Form
. I can create a Form
and Handler
with a circular reference between them like so:
Form f = new Form();
Handler.AttachWithCircularReference(f);
f.Show();
I've got an application with a main test form that runs that exact code when a button is clicked, and which has another button
that forces a GC. If I run this and force a GC before I close the 'f
' Form
, then of course both the
Handler
and the Form
remain alive. But if I close the Form
and then
force a GC, the Handler
's finalizer runs. This is because closing a modeless Form
causes
Windows Forms not only to call Dispose
on the Form
, but also to cease to hold any references
to the Form
. (Windows Forms keeps forms reachable while they are open, but lets them go as soon as
they close.)
So, there we have it. Proof that circular references between forms and their event handlers are not sufficient to cause memory leaks.
So where does this myth come from? Well it's easy enough to cause a leak. We can simply cause the form to remain reachable. Consider this variation:
private ArrayList keptForms = new ArrayList(); private void btnNewKeep_Click(object sender, System.EventArgs e) { Form f = new Form(); Handler.AttachWithCircularReference(f); keptForms.Add(f); f.Show(); }
This is the code for another of the buttons on my test rig. This does more or less the same as before, but it dumps
a reference to the Form
in an ArrayList
. That ArrayList
is a member of
my main form, so as long as my main form is open, the ArrayList
will be reachable, as will everything
the ArrayList
. So now, I'm preventing the Form
from being collected, which in turn also
prevents the associated Handler
from being collected.
This is the kind of scenario that has given rise to this myth that event handler circular references cause leaks. However, the circular reference is completely irrelevant here. Consider this example:
Form f = new Form();
Handler.AttachWithUnidirectionalReference(f);
keptForms.Add(f);
f.Show();
This creates a Handler
which handles the Click
event from the Form
as
before, but which does not hold a reference back to the Form
. So there is
no circular reference here. There's a chain: Windows Forms is keeping my main form reachable as long
as it is open. The main form keeps the ArrayList
reachable. That keeps the Form
created in this code snippet reachable. That Form
keeps the Handler
reachable. There
are no circular references, but this will keep the Handler
alive. If that wasn't your intention, then you might
regard this as being a memory leak.
So circular references do not cause memory leaks. And it is quite possible to leak memory without setting up a circular reference. Both of these statements are true regardless of whether the references are normal explicit object references, or references held via a delegate.
You can download the code I'm using to test all this from here.