Reputation: 2330
This is perhaps a question of semantics, but perhaps not, so I ask: Is there an appreciable difference in the following 2 snippets?
public Parent()
{
Child newChild = new Child();
newChild.RequestSpecialEvent += (sender, e) =>
{
newChild.DoMagic();
}
}
or
public Parent()
{
Child newChild = new Child();
newChild.RequestSpecialEvent += (sender, e) =>
{
((Child)sender).DoMagic();
}
}
Obvious difference is option 1 sort of self-references itself, while option 2 performs a cast on an object. Performance wise, I expect the cast is more expensive.
However, I'm theorizing that in option 1, it is technically the "Parent" which holds a reference to "newChild" (through the delegate defined within Parent), so even if newChild goes away (newChild = null or something similar), the newChild object can't be garbage collected (gc'ed) because Parent has defined a delegate which is still attached to it. newChild can only be gc'ed when Parent eventually goes away.
However, in option 2, Parent never creates such a "hard reference" to newChild, so when newChild = null happens, it truly can be gc'ed immediately.
I prefer option 1 for its succinctness and readability, but worry that option 2 would be better. Thoughts? Is my theory correct or off-base? Is there an alternative or more preferred approach (with sound reasoning) for declaring the same event listener relationship with parent/child classes?
Response to @StriplingWarrior:
In regards to the garbage collection, I'm still a little skeptical. The delegate references newChild, so to me it seems newChild can't go away until the delegate goes away. Now the delegate will go away if newChild goes away...but still newChild can't go away until delegate goes away! Seems circular (almost). Seems like this would have to happen:
//newChild = null;
//This alone won't truly free up the newChild object because the delegate still
//points to the newChild object.
//Instead, this has to happen
newChild.RequestSpecialEvent = null; //destroys circular reference to newChild
newChild = null; //truly lets newChild object be gc'd
Or maybe by saying 'newChild = null;' only, newChild.RequestSpecialEvent stops pointing at the delegate, which allows the delegate to go away, which then allows newChild to go away? Maybe I just talked myself into your answer. :)
Upvotes: 3
Views: 1007
Reputation: 16107
Circular references are not a problem for the .Net GC, as it doesn't use reference counting to identify living objects. The GC will identify references that are guaranteed to still be in use, called Roots (eg static references, references on the stack of currently executing methods, etc). The objects these roots refer to are guaranteed to be alive. Transitively, the objects those objects refer to are guaranteed to be alive. Therefore, the GC can trace the path from the roots out through all objects known to be alive. The rest are dead.
In your circular reference example, there is no root pointing to newChild. Therefore, it will be eligible for collection, even though the event handler refers back to newChild, which refers to the event handler, which refers to newChild...
The Parent class in your example doesn't keep any reference to newChild - it's just created as a local in the constructor of Parent. So it should be eligible for collection straight after assigning the event handler.
This is a good article on the GC: http://msdn.microsoft.com/en-us/magazine/bb985010.aspx
Upvotes: 2
Reputation: 156459
Your thoughts seem pretty much spot-on, except that I'm pretty sure newChild
could still be garbage-collected in option 1, because the delegate that references it is itself only referenced by a handler on newChild
itself.
The two snippets are functionally equivalent, with Option 1 using slightly more memory due to the extra reference in the delegate, but going slightly faster when invoked since it avoids the cast. The difference either way is so negligible that I'd suggest using whichever feels cleanest (#1, if you ask me).
The only time I'd go with something like #2 is if you want to apply the same delegate to a number of controls. In that case, this would end up using less memory:
var handler = new RequestSpecialEventHandler((sender, e) =>
{
((Child)sender).DoMagic();
});
foreach(var child in children)
{
child.RequestSpecialEvent += handler;
}
And one other caveat to be aware of is that since Option #1 refers to the newChild
variable, if the value of that variable changes later in the method, the new value will be used when the handler is invoked. For example, in this example:
foreach(var child in children)
{
child.RequestSpecialEvent += (sender, e) =>
{
// BEWARE: Don't do this! Modified closure!
child.DoMagic();
};
}
... Any time the event fires on any of these children, "Magic" will be performed N times only on the last child in the children
collection.
Upvotes: 2