Trending
Opinion: How will Project 2025 impact game developers?
The Heritage Foundation's manifesto for the possible next administration could do great harm to many, including large portions of the game development community.
Delegates make implementing the Observer pattern in C# very easy, but many code samples (especially for Unity) fail to encapsulate the list of observers properly. C#'s "event" keyword comes to the rescue.
For the purposes of this post, I'm assuming the reader is already familiar with delegates in C# and the Observer pattern.
I'll be using the word "event" in two different contexts: when a particular state is reached within a subject or a behavior is invoked that observers may wish to be notified of, and later the event keyword in C#.
For the code samples I use a partially written Button class for a hypothetical GUI framework. The would-be author of Button wishes to define a Clicked event on the class so that observers can respond to (you guessed it) the button being clicked by the user.
public class Button { public delegate void ClickedEventHandler(); // Public field of a delegate type. Never do this! public ClickedEventHandler Clicked; // ... protected void OnClicked() { // Trigger the Clicked event. if (Clicked != null) { Clicked(); } } }
In the above example, a nested delegate type named ClickedEventHandler is defined and a public field of this delegate type named Clicked is declared. A protected method named OnClicked is provided by Button as a convenience for it or any sub-classes to trigger the event.
Given a variable button of type Button, observers are expected to subscribe to the event like so:
void button_Clicked() { // Respond to the button being clicked... } // ... button.Clicked += button_Clicked; // Subscribe to the Clicked event. button.Clicked -= button_Clicked; // Unsubscribe from the Clicked event.
But what happens if a client coder accidentally uses = instead of +=?
button.Clicked = button_Clicked;
The answer: Any prior observers subscribed to the event are lost and replaced by the single new observer! But it gets worse. Client code can trigger the event directly!
button.Clicked(); // R.I.P. encapsulation.
Only the Button class or its sub-classes should be able to do that.
Enter C#'s event keyword. Similarly to how properties are syntactic wrappers for get and set methods, C# events are syntactic wrappers for add and remove methods. Let's define one now:
public class Button { public delegate void ClickedEventHandler(); private ClickedEventHandler clicked; public event ClickedEventHandler Clicked { add { clicked += value; } remove { clicked -= value; } } // ... protected void OnClicked() { // Trigger the Clicked event. if (clicked != null) { clicked(); } } }
Our field of type ClickedEventHandler is now private and renamed clicked in lowercase. The Clicked event wraps the field and modifies it via its add and remove methods. Just as set methods have an implicit variable named value of the property type, so to do add and remove methods have such a variable of the event type.
Client code still subscribes observers to the event just like before, but that's now all it can do.
void button_Clicked() { // Respond to the button being clicked... } // ... button.Clicked += button_Clicked; // Invokes the event's add method. button.Clicked -= button_Clicked; // Invokes the event's remove method. // ... button.Clicked = button_Clicked; // Won't compile! button.Clicked(); // Won't compile!
But, we don't actually have to define add and remove methods explicitly. If we leave them out, the compiler will define them and a corresponding field behind the scenes for us. The add and remove methods that it defines are much like the example (but thread-safe).
We can declare and trigger the event like so:
public class Button { public delegate void ClickedEventHandler(); public event ClickedEventHandler Clicked; // ... protected void OnClicked() { // Trigger the Clicked event. if (Clicked != null) { Clicked(); } } }
From the Button class's perspective, Clicked is a field. But from the perspective of client code, Clicked is an event that observers can be subscribed to and unsubscribed from only, just as before when we defined our own add and remove methods.
If you've paid close attention, you may have noticed that the only difference between the first code sample and the last is the event keyword! But now you know what it does, how it does it, and how to customize its behaviour.
So why would anyone ever want to write there own add and remove methods for an event? The only circumstance where I've ever needed to, is to add observers as individual items to a list, and I've only ever wanted to do that for two reasons: to avoid memory allocations when two non-null delegates are combined, or because the underlying delegate returns objects that I want to process for every observer (by default, only the return values of the last method in a delegate's invocation list are returned to the caller, while all others are ignored).
Read more about:
BlogsYou May Also Like