Introduction
If you don't know what delegates and events are, don't read on. Go read an introductory book on C# first. If you do know what events and delegates are and you have used them successfully, but still have some questions about them, this article may shed some light.
Delegates and events
MSDN holds some pretty accurate definitions for delegates and events, as it should:
A delegate declaration defines a reference type that can be used to encapsulate a method with a specific signature. A delegate instance encapsulates a static or an instance method. Delegates are roughly similar to function pointers in C++. However, delegates are type-safe and secure.
Events are used on classes and structs to notify objects of occurrences that may affect their state. To add an event to a class requires using the event keyword and providing a delegate type and a name for the event.
To recap that in layman's terms: a delegate contains the reference to an object and a method that can operate on that object -- say "a function pointer" and a "this" -- whereas an event resembles a collection capable of holding such delegates and firing them one after another. As an example, one can install an "event handler" to be called on a mouse click using the following lines of code:
public class myForm : Form
{
private Button button=new Button();
...
public myForm()
{
...
button.Click+=new EventHandler(button_Click);
...
}
private void button_Click(object sender, EventArgs e)
{
...
}
}
A popular mistake: adding the same delegate more than once
Normally, you want a delegate to be called just once: one btn_click()
for every click of the button. When you add the same delegate more than once to an event, the handler will be called more than once. Most of the time, that was not the intention and the extra adds are a mistake. That mistake may constitute a bug: hitting the "A" key should not enter two characters "A" in a textbox. Simple mistakes like that will be noticed immediately.
Sometimes, executing the handler more than once does not result in an error; it just wastes CPU cycles. For instance, executing a Focus
handler twice probably will not result in a logical error. Of course, if for some reason you keep adding the same Paint
handler over and over to the Paint
event, in the end the responsiveness of the application will be completely ruined. It may take you a while to figure out where the mistake is!
A little magic
The event works somewhat like a collection: you can add delegates to it and, later on when you are no longer interested, you can remove them again. Some people scrupulously store the delegate they are about to add to the event so that they can later use the reference for removing it again, as in:
private EventHandler buttonClickHandler =
new EventHandler(button_Click);
...
button.Click += buttonClickHandler;
...
button.Click -= buttonClickHandler;
Other people, and most books on C#, use a different approach: they create a delegate and add it. Later on, they create another delegate and remove it, as in:
button.Click += new EventHandler(button_Click);
...
button.Click -= new EventHandler(button_Click);
So basically now the events "collection" seems able to remove an object that is different from, but equivalent to, another object that was added earlier. What this actually means is that the run-time system, when executing the -=
line, parses the delegate and looks for one with the same object and method pointer, i.e. this
and button_click
respectively. If it finds one or more, it removes one. If not, nothing happens and no exception gets thrown. All of this gets confirmed by the test program.
The test program
The program is a very simple console application. It creates one button that does not get a parent and hence remains invisible. The program then plays around with a delegate to handle the click events. It simulates some clicks using the Button.PerformClick()
method, which fortunately also works for invisible buttons! Most importantly, the program logs all that is going on, so the log can confirm the theory. This is the heart of the test sequence:
public void Run()
{
doClick();
addDelegate();
doClick();
addDelegate();
doClick();
removeDelegate();
doClick();
removeDelegate();
doClick();
removeDelegate();
doClick();
log("Done");
}
private static void log(string s)
{
if (s.Length!=0) s=DateTime.Now.ToString("ss.fff ")+s;
Console.WriteLine(s);
}
private void doClick()
{
log("CLICK");
btn.PerformClick();
}
private void addDelegate()
{
delegateCount++;
log("ADD DELEGATE #"+delegateCount);
btn.Click+=new EventHandler(btn_Click);
}
private void removeDelegate()
{
log("REMOVE DELEGATE #"+delegateCount);
delegateCount--;
try {btn.Click-=new EventHandler(btn_Click);}
catch { log("failed to remove handler"); }
}
private void btn_Click(object sender, EventArgs e)
{
clicks++;
log(" got click #"+clicks);
}
And this is the log that gets produced:
29.750 CLICK
29.750 ADD DELEGATE #1
29.750 CLICK
29.750 got click #1
29.750 ADD DELEGATE #2
29.750 CLICK
29.750 got click #2
29.750 got click #3
29.750 REMOVE DELEGATE #2
29.750 CLICK
29.750 got click #4
29.750 REMOVE DELEGATE #1
29.750 CLICK
29.750 REMOVE DELEGATE #0
29.750 CLICK
29.750 Done
These are the observations we made:
- Every
CLICK
is followed by a number of "got click" messages equal to the number of delegates currently in the event: first none and then 1, 2, 1, 0
- The first "remove delegate" did not remove all delegates; it removed just one
- Removing more delegates than we added in the first place did not throw an exception; it had no effect at all
C# 2.0
Originally, C# required an explicit delegate to be added to or removed from an event, as in:
button.Click += new EventHandler(button_Click);
...
button.Click -= new EventHandler(button_Click);
The above works for all .NET versions. The new version of C#, C# 2.0, also accepts a shorthand notation, as in:
button.Click += button_Click;
...
button.Click -= button_Click;
This generates exactly the same MSIL code. The compiler deduces which delegate needs to be instantiated from the declaration of the Click
event itself. So, the magic described earlier is still going on. A new delegate is being created in order to remove an earlier one, but this is no longer apparent from the source code! It now seems like the button_click
first got added and then removed again.
The final question: do we have to remove event handlers?
Of course we will remove a delegate if the object remains active, but does not want to receive the notifications anymore. However, what if the object is basically done; it has no use anymore and we hope it will soon be garbage collected? We realize that a delegate contains a reference to the object to which the method applies, so will the existence of delegates prevent it from being collected? There are two different situations:
- Most of the time, all delegates are used internally and the situation is simple. In our earlier example, the button is part of a class and the click handler method also belongs to that class. So in a sense it is an "internal delegate" and an "internal event," something that is invisible from the outside. When an instance of that class no longer is reachable by the
Main()
method and its descendants, then the object is a candidate for garbage collection. The event inside the object should not and will not prevent that.
- As soon as one delegate is used "outside" of its class, the situation is different. If an instance of
class1
attaches its delegate to an event of another class, i.e. class2
, then that counts as a reference from class2
to class1
. Hence, as long as the class2
object is alive, it will keep the instance of class1 alive. To end this dependency, the class1
instance should remove its delegate.
Conclusion: you should remove delegates from events when they reach outside the class itself; i.e. when you subscribe to external events, you should end your subscription when you are done. Failing to do so will keep your object alive longer than necessary.
Other points of interest
The following items relate to recent topics on one of the CodeProject message boards. Note that some of them are not discussed in this article, but are present in the source code:
- Create a simple
log()
method to get timestamps on major actions
- Add
Console.ReadLine()
to keep the command window from closingng
- Use
#define
, #if
, #else
, #endif
statements for conditional compilation
History
- 24 July, 2007 -- LP#EventHandlerRemoval 1.0, first release