Thursday, April 25, 2024 22:01

Table of contents >> Delegates, Lambda Expressions, Events > Events

Events

Events are a more secure way of implementing the observer pattern described in the previous lesson, and they are the evolutionary step of raw delegates. You may have heard about the event-driven programming as a concept that describes a programming paradigm in which the flow of the program is determined by events such as user actions (mouse clicks, key presses), sensor outputs, or messages from other programs or threads. Events are at the heart of this concept, and in this lesson I will try to explain them in a simple manner, since, despite being quite easy to comprehend, the majority of junior/medium programmers misunderstand and fear them.

I was saying that events are a safer way of implementing the observer pattern implemented with raw delegates, which I described in the previous lesson. Why did I say “safer”? Is there something we need to be aware regarding observer pattern with raw delegates? Well, I implemented a simple system in which a train signal would alert any subscriber car that a train is coming. These were the codes we ended up at the end of last lesson:

In real life, let’s imagine the following scenario: some playful kid comes and pulls the lever and the train signal would go “Ding, Ding Ding!” even if no train was coming, and the barriers would lower, and all the fuss, completely wasting everybody’s time. But, as a kid, I suppose that might be fun.

So, in my current code, let me show you that any kid can come up and trigger this train signal whenever they feel like so, even when there is no train coming:

The proper way of triggering the train signal is to call the trainSignal.HereComesATrain() method, that invokes the TrainsAreComing delegate. But as a bad kid, what stops me from invoking it directly, without calling the method that actually performs some checks before triggering the signal? Since TrainsAreComing is public, I can call it directly: trainSignal.TrainsAreComing();, can’t I?

Your first instinct would be to make TrainsAreComing delegate private, but think about it for a second. If the delegate is private, that means that we can’t subscribe to it inside the constructor of our Car instances.

So, what’s the solution? Will we just let any kid play with the train signal and continue wasting everybody’s time?

Even worse, and quite deadly in this example, what if after I trigger the train signal a few times, I giggle and I run, I get bored and I decide to do this:

Think about it for a moment… TrainsAreComing is referencing a delegate chain, but once I assign null to it, it references nothing.

Finally, suppose later on, a train legitimately is coming, and we want to call HereComesATrain() method. Well, remember, () on a delegate is just a short form or calling the Invoke() method on that delegate instance:

I don’t have to tell you what happens when we try to use the dot (.) operator on null references: your program will crash with a NullReferenceException‘Object reference not set to an instance of an object.’.

We could perform a check to see if the delegate is not null before invoking it, but that would only save us from crashing the program, it will still not stop the kid from triggering the signal for fun or getting unsignaled cars crashed by the train.

In this case, C# comes to the rescue, and we only have to add a single keyword to the delegate declaration:

The keyword event will inform the compiler that this is no longer a raw delegate, but a special form of it. And the first sign that something changed with our code is the fact that now you will get some errors for a few lines of code, in Visual Studio, specifically, these lines:

and

The error will simply state “Error CS0070 The event ‘TrainSignal.TrainsAreComing’ can only appear on the left hand side of += or -= (except when used from within the type ‘TrainSignal’)“.

So, the first thing you need to learn about events: they stop you from invoking them directly, and they stop you from assigning to them directly. With one keyword, we stopped the kid from triggering the signal, and also from deciding who and if gets notified of the train signal.

The second thing you need to learn about delegates is that they only allow other objects to subscribe and unsubscribe to them:

At this point, if any employer asks you in an interview “What’s the difference between a delegate and an event?”, the perfect answer would be “an event is a delegate reference with two restrictions on it: you cannot invoke the delegate reference directly and you cannot assign to it directly”.

Now, let’s dig a bit deeper into this and see how the compiler manages to do all that. We will once again use a tool called ILSpy, which lets us see MSIL (Microsoft Intermediate Language). First, I’ll change my codes a bit, to have the simplest of the simplest example, for understanding everything easier:

I will also change my program’s output type from Console Application to Class Library, so that I will create a .DLL file instead of an .EXE one. You may not know, but in .NET, the only difference between a Console Application and Class Library is the fact that the first one has an application entry point represented by the Main() method, while the other doesn’t. I am choosing to output to a library specifically to also remove the Main() method and leave only the relevant delegate instance and the class surrounding it.

Now, when I open my DLL inside ILSpy, I see these codes in C#:

C# MSIL delegateThe code is identical to my code in Visual Studio. If I switch to IL, the situation changes a bit:

IL MSIL delegatebut if we analyze the code carefully, even if we don’t know IL language, we can see a class declaration named HelloWorld.TrainSignal, with a constructor ctor() and a field (variable declared directly inside a class) of type Action, named TrainsAreComing. So, basically, the same codes as our program, translated.

Next, I will modify my code by adding the event keyword:

After I recompile my program and I reopen it in ILSpy, I will see this, for C#:

C# MSIL eventSo, still similar codes to my program, but when I switch to IL, I’ll see these:

IL MSIL eventNow, the situation changed quite a bit! We still have the field of type Action, named TrainsAreComing, but this time we notice that it is private, which means it can only be accessed directly from inside our TrainSignal class. This is the reason why we cannot invoke or assign to our delegate directly.

Aside of that, the other thing that is important to us is this piece of code:

The .addon and .removeon are two metadata constructs that basically tell the compiler that whenever someone wants to subscribe or unsubscribe to our event, it must call some methods named add_TrainsAreComing() and remove_TrainsAreComing(), and this is the reason why we are only able to += and -= to our events.

For completenes, let’s take a look at add_TrainsAreComing():

At a short inspection of the code, we can notice some Delegate::Combine, which I already described when I talked about delegate chaining. It basically adds a method to the chains of methods of a delegate.

One thing you should be aware of is the fact that each event , since it is basically a reference, it needs 4 bytes of storage. In some languages and frameworks, you will notice a pattern known as hooking, where there is a trio of events, such as BeginLoading, Loaded, EndLoading or BeginDragging, Dragging, EndDragging (the principle is related to “hooking” to or into an object, performing some action and then “unhooking” from that object). In other words, programmers are usually no shy of using A LOT of events for their classes/UI controls. While that may not seem like a problem in daily use, let’s consider the following scenario: you have a database of 10.000 users and you want to display them in some window, using a Listview, for instance. Each Listview item displays a user, and it is implemented using a custom class the programmer writes. Now, consider the programmer implements an event in that class, named Click, so that the users can click on their username row and open up a window displaying the details of their account. Since the event requires 4 bytes to be stored, and since the class is instantiated for each of the user rows in the table, that means 10.000 user rows * 4 bytes per event = 40.000 bytes, just to store the references of those events! 40k bytes may not seem much, but that’s just a single event, and we are not counting up the rest of the class objects. What if the database contained 100.000 users? 1.000.000?! You can clearly see that storage can get out of hand pretty quickly in such circumstances. So, when declaring events inside objects, always keep in mind if those objects will be instantiated a huge number of times or not. As a thought exercise, you should know that the Button class in WPF has around 100 events, give or take. That’s 400 bytes for each button, just for declaring the events. Now, you can wonder what would happen if we were to put a single button on each row of our 10.000 users table…

Another thing that you should always keep in mind is unsubscribing to the events you have subscribed to. Consider you have a class called Person, which has an event handler method named DoSomething(). In you Main() method, you instantiate a bunch of these classes, and subscribe their DoSomething() methods to some events, like this:

You might think that once we click the button and its Click event fires, and the DoSomething() method is done doing what it does, the Person instance will no longer be needed, and the Garbage Collector will come up and clean it up, freeing the memory it occupies. But that is a nasty trap, because our window holds a reference to the button, and the button’s Click event is just a delegate that holds a reference to the Person object and the Person‘s DoSomething() method, and since we are not unsubscribing these DoSomething() methods from our Click event, they can not be disposed and recycled, because the event keeps a reference to them! every time we click the button, we create a new Person, whose DoSomething() method is added to the delegate chain of the Click event. If we keep clicking, at some point, we will run out of RAM! So, whenever you are done using some event handler method, make sure you unsubscribe it from the delegate chain, or it and the object that it belongs to, will continue to occupy your memory!

In the next lesson, I will explain more aspects of adding and removing from events.

Tags: , , ,

Leave a Reply



Follow the white rabbit