Gamasutra: The Art & Business of Making Gamesspacer
View All     RSS
November 20, 2017
arrowPress Releases






If you enjoy reading this site, you might also want to check out these UBM Tech sites:


 

Events in C#: you're probably doing it wrong

by Christopher Myburgh on 09/12/17 01:27:00 pm

The following blog post, unless otherwise noted, was written by a member of Gamasutra’s community.
The thoughts and opinions expressed are those of the writer and not Gamasutra or its parent company.

 

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.
 

Defining an event: The dangerous way.

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.
 

Defining an event: The safe way.

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!

 

Defining an event: The simple (but still safe) way.

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.
 

When to define explicit add and remove methods.

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).


Related Jobs

Titan IM
Titan IM — Port Stephens, New South Wales, Australia
[11.19.17]

Senior Game/Software Programmer
TheWaveVR
TheWaveVR — Austin, Texas, United States
[11.18.17]

SOFTWARE ENGINEER GENERALIST
TheWaveVR
TheWaveVR — Austin, Texas, United States
[11.18.17]

SERVER PLATFORM ENGINEER
2K
2K — Novato, California, United States
[11.17.17]

LEAD TOOLS AND PIPELINE ENGINEER





Loading Comments

loader image