.NET events in detail
If you are a .NET programmer, then you must have declared and used events in your code. Despite this, not everyone knows how events work inside and what features are associated with their application. In this article, I tried to describe the operation of events in as much detail as possible, including some special cases that you rarely have to deal with, but about which it is important and / or interesting to know.
An event in C # is an entity that provides two possibilities: for a class, to report changes, and for its users, to respond to them.
Event announcement example:
Consider what an ad consists of. Event modifiers come first, then the event keyword, followed by the event type, which must be a delegate type, and the event identifier, that is, its name. The event keyword tells the compiler that this is not a public field, but a specially designed structure that hides from the programmer details of the implementation of the event mechanism. In order to understand how this mechanism works, it is necessary to study the principles of work of delegates.
We can say that a delegate in .NET is a kind of analogue of a function reference in C ++. However, such a definition is inaccurate, because each delegate can refer not to one, but to an arbitrary number of methods that are stored in the invocation list of the delegate. The delegate type describes the signature of the method to which it can refer; instances of this type have their own methods, properties, and operators. When you call the Invoke () method, a sequential call to each of the list methods is performed. A delegate can be called as a function; the compiler translates such a call into an Invoke () call.
In C #, for delegates, there are + and - operators that do not exist in the .NET environment and are the syntactic sugar of the language, expanding into a call to the Delegate.Combine and Delegate.Remove methods, respectively. These methods allow you to add and remove methods in the call list. Of course, the form of operators with assignment (+ = and - =) is also applicable to delegate operators, as well as to operators defined in the .NET environment + and - for other types. If, when subtracting from a delegate, his call list is empty, then null is assigned to it.
Consider a simple example:
Events in C # can be defined in two ways:
Consider the most commonly used event implementation - implicit. Suppose you have the following source code in C # 4 (this is important, a slightly different code is generated for earlier versions, which will be described later):
These lines will be translated by the compiler into code similar to the following:
The add block is called when subscribing to an event, the remove block is called when unsubscribing. These blocks are compiled into separate methods with unique names. Both of these methods accept one parameter - a delegate of the type corresponding to the type of the event and have no return value. The parameter name is always ”value”, an attempt to declare a local variable with this name will result in a compilation error. The scope indicated to the left of the event keyword defines the scope of these methods. A delegate is also created with the name of the event, which is always private. That is why we cannot call an event implemented in an implicit way from the class inheritor.
Interlocked.CompareExchange compares the first argument with the third and, if they are equal, replaces the first argument with the second. This action is thread safe. The loop is used for the case when, after assigning the delegate to the comparand variable of the event and before performing the comparison, another thread modifies this delegate. In this case, Interlocked.CompareExchange does not replace, the loop boundary condition is not satisfied, and the next attempt occurs.
When an event is explicitly implemented, the programmer declares a delegate field for the event and manually adds or removes subscribers through add / remove blocks, both of which must be present. Such an ad is often used to create its own event mechanism while maintaining the convenience of C # in working with them.
For example, one of the typical implementationsconsists in a separate storage of the dictionary of event delegates, in which only those delegates are present for the events of which they were subscribed. The dictionary is accessed using keys, which are usually static fields of type object, used only to compare their references. This is done in order to reduce the amount of memory occupied by an instance of the class (in case it contains a large number of non-static events). This implementation is used in WinForms.
All subscription and unsubscribing actions (denoted as + = and - =, which can be easily confused with delegate operators) are compiled into calls to the add and remove methods. Calls inside the class other than the above are compiled into simple work with a delegate. It should be noted that with an implicit (and with the correct explicit) event implementation, it is impossible to access the delegate from outside the class, you can work only with the event as an abstraction - by signing and unsubscribing. Since there is no way to determine whether we have subscribed to any event (if you do not use reflection), it seems logical that unsubscribing from it will never cause errors - you can safely unsubscribe, even if the event delegate is empty.
Scope modifiers (public, protected, private, internal) can be used for events, they can be overlapped (virtual, override, sealed) or not implemented (abstract, extern). An event can overlap an event with the same name from the base class (new) or be a member of the class (static). If an event is declared with both the override modifier and the abstract modifier at the same time, then the heirs of the class will have to override it (as well as methods or properties with these two modifiers).
As already noted, the type of event must always be the type of delegate. The standard types for events are the EventHandler and EventHandler <TEventArgs> types where TEventArgs is the descendant of EventArgs. The EventHandler type is used when event arguments are not provided, and the EventHandler <TEventArgs> type is used when there are event arguments, then a separate class is created for them - an inheritor from EventArgs. You can also use any other types of delegates, but using the typed EventHandler <TEventArgs> looks more logical and beautiful.
The implementation of the field-like event described above corresponds to the C # 4 language (.NET 4.0). For earlier versions, there are very significant differences.
An implicit implementation uses lock (this) to provide thread safety instead of Interlocked.CompareExchange with a loop. For static events, lock (typeof (Class)) is used. Here is code similar to the compiler’s implicit event definition in C # 3:
In addition, the work with the event inside the class is carried out as with a delegate, i.e. + = and - = call Delegate.Combine and Delegate.Remove directly, bypassing the add / remove methods. This change may make it impossible to build a project in C # 4! In C # 3, the result of + = and - = was a delegate, because The result of assigning a variable is always the assigned value. In C # 4, the result is void, because add / remove methods do not return values.
In addition to changes in the work on different versions of the language, there are several more features.
When subscribing to an event, we add to the call list of the event delegate a reference to the method that will be called when the event is called. Thus, the memory occupied by the object subscribing to the event will not be freed until it is unsubscribed from the event or until the destruction of the object containing the event. This feature is one of the common causes of memory leaks in applications.
To correct this drawback, weak events are often used, weak events. This topic has already been covered on Habré .
An event that is part of an interface cannot be implemented as a field when this interface is explicitly implemented. In such cases, you should either copy the standard event implementation for implementation as a property, or implement this part of the interface implicitly. Also, if you do not need the thread safety of this event, you can use the simplest and most effective definition:
Events before the call should be checked for null, which follows from the work of delegates described above. The code grows from this, to avoid which there are at least two ways. The first method is described by Jon Skeet in his book C # in depth :
Short and concise. We initialize the event delegate with an empty method, so it will never be null. It is impossible to subtract this method from the delegate, because it is defined during the initialization of the delegate and it has neither a name nor a link to it from anywhere in the program.
The second way is to write a method that contains the necessary null check. This technique works especially well in .NET 3.5 and later, where extension methods are available. Since when calling the extension method, the object on which it is called is just a parameter of this method, this object can be an empty reference, which is used in this case.
Thus, we can trigger events like Changed.SafeRaise (this, EventArgs.Empty), which saves us lines of code. You can also define the third variant of the extension method for the case when we have EventArgs.Empty, so as not to pass them explicitly. Then the code will be reduced to Changed.SafeRaise (this), but I will not recommend this approach, because for other members of your team, this may not be as explicit as passing an empty argument.
If you have ReSharper, then you could watch his next message . The resolver team correctly believes that not all of your users are sufficiently informed about the work of events / delegates in terms of unsubscribing / subtraction, but nevertheless, your events should work predictably not for your users, but from the point of view of events in .NET, but because . there is such a feature, then it should remain in your code.
In the first beta of C # 4, Microsoft tried to add contravariance to events. This allowed you to subscribe to the EventHandler <MyEventArgs> event using methods that have the EventHandler <EventArgs> signature and everything worked until several methods with a different (but suitable) signature were added to the event delegate. Such code compiled, but crashed with a runtime error. Apparently, they could not get around this and in the release of C # 4, contravariance for EventHandler was disabled.
This is not so noticeable if you omit the explicit creation of a delegate when subscribing, for example, the following code compiles fine:
This is because the compiler itself will substitute new EventHandler <MyEventArgs> (...) to both subscriptions. If you use new EventHandler <EventArgs> (...) in at least one of the places, the compiler will report an error - it is impossible to convert the type of EventHandler <System.EventArgs> to EventHandler <Events.MyEventArgs> (here Events is the namespace of my test project).
The following is a list of sources, some of which were used in the preparation of the article. I recommend reading the book by Jon Skeet (Jon Skeet), which describes in detail not only the delegates, but also many other language tools.
What is an event?
An event in C # is an entity that provides two possibilities: for a class, to report changes, and for its users, to respond to them.
Event announcement example:
publicevent EventHandler Changed;
Consider what an ad consists of. Event modifiers come first, then the event keyword, followed by the event type, which must be a delegate type, and the event identifier, that is, its name. The event keyword tells the compiler that this is not a public field, but a specially designed structure that hides from the programmer details of the implementation of the event mechanism. In order to understand how this mechanism works, it is necessary to study the principles of work of delegates.
Event Workbench - Delegates
We can say that a delegate in .NET is a kind of analogue of a function reference in C ++. However, such a definition is inaccurate, because each delegate can refer not to one, but to an arbitrary number of methods that are stored in the invocation list of the delegate. The delegate type describes the signature of the method to which it can refer; instances of this type have their own methods, properties, and operators. When you call the Invoke () method, a sequential call to each of the list methods is performed. A delegate can be called as a function; the compiler translates such a call into an Invoke () call.
In C #, for delegates, there are + and - operators that do not exist in the .NET environment and are the syntactic sugar of the language, expanding into a call to the Delegate.Combine and Delegate.Remove methods, respectively. These methods allow you to add and remove methods in the call list. Of course, the form of operators with assignment (+ = and - =) is also applicable to delegate operators, as well as to operators defined in the .NET environment + and - for other types. If, when subtracting from a delegate, his call list is empty, then null is assigned to it.
Consider a simple example:
Action a = () => Console.Write("A"); //Action объявлен как public delegate void Action();Action b = a;
Actionc = a + b;
Action d = a - b;
a(); //выведет A
b(); //выведет Ac(); //выведет AA
d(); //произойдет исключение NullReferenceException, т.к. d == null
Events - default implementation
Events in C # can be defined in two ways:
- Implicit implementation of an event (field-like event).
- Explicit event implementation.
Consider the most commonly used event implementation - implicit. Suppose you have the following source code in C # 4 (this is important, a slightly different code is generated for earlier versions, which will be described later):
classClass {
publicevent EventHandler Changed;
}
These lines will be translated by the compiler into code similar to the following:
classClass {
EventHandler сhanged;
publicevent EventHandler Changed {
add {
EventHandler eventHandler = this.changed;
EventHandler comparand;
do {
comparand = eventHandler;
eventHandler = Interlocked.CompareExchange<EventHandler>(refthis.changed,
comparand + value, comparand);
} while(eventHandler != comparand);
}
remove {
EventHandler eventHandler = this.changed;
EventHandler comparand;
do {
comparand = eventHandler;
eventHandler = Interlocked.CompareExchange<EventHandler>(refthis.changed,
comparand - value, comparand);
} while (eventHandler != comparand);
}
}
}
The add block is called when subscribing to an event, the remove block is called when unsubscribing. These blocks are compiled into separate methods with unique names. Both of these methods accept one parameter - a delegate of the type corresponding to the type of the event and have no return value. The parameter name is always ”value”, an attempt to declare a local variable with this name will result in a compilation error. The scope indicated to the left of the event keyword defines the scope of these methods. A delegate is also created with the name of the event, which is always private. That is why we cannot call an event implemented in an implicit way from the class inheritor.
Interlocked.CompareExchange compares the first argument with the third and, if they are equal, replaces the first argument with the second. This action is thread safe. The loop is used for the case when, after assigning the delegate to the comparand variable of the event and before performing the comparison, another thread modifies this delegate. In this case, Interlocked.CompareExchange does not replace, the loop boundary condition is not satisfied, and the next attempt occurs.
Ad with add and remove
When an event is explicitly implemented, the programmer declares a delegate field for the event and manually adds or removes subscribers through add / remove blocks, both of which must be present. Such an ad is often used to create its own event mechanism while maintaining the convenience of C # in working with them.
For example, one of the typical implementationsconsists in a separate storage of the dictionary of event delegates, in which only those delegates are present for the events of which they were subscribed. The dictionary is accessed using keys, which are usually static fields of type object, used only to compare their references. This is done in order to reduce the amount of memory occupied by an instance of the class (in case it contains a large number of non-static events). This implementation is used in WinForms.
How is the event subscribed and called?
All subscription and unsubscribing actions (denoted as + = and - =, which can be easily confused with delegate operators) are compiled into calls to the add and remove methods. Calls inside the class other than the above are compiled into simple work with a delegate. It should be noted that with an implicit (and with the correct explicit) event implementation, it is impossible to access the delegate from outside the class, you can work only with the event as an abstraction - by signing and unsubscribing. Since there is no way to determine whether we have subscribed to any event (if you do not use reflection), it seems logical that unsubscribing from it will never cause errors - you can safely unsubscribe, even if the event delegate is empty.
Event modifiers
Scope modifiers (public, protected, private, internal) can be used for events, they can be overlapped (virtual, override, sealed) or not implemented (abstract, extern). An event can overlap an event with the same name from the base class (new) or be a member of the class (static). If an event is declared with both the override modifier and the abstract modifier at the same time, then the heirs of the class will have to override it (as well as methods or properties with these two modifiers).
What types of events are there?
As already noted, the type of event must always be the type of delegate. The standard types for events are the EventHandler and EventHandler <TEventArgs> types where TEventArgs is the descendant of EventArgs. The EventHandler type is used when event arguments are not provided, and the EventHandler <TEventArgs> type is used when there are event arguments, then a separate class is created for them - an inheritor from EventArgs. You can also use any other types of delegates, but using the typed EventHandler <TEventArgs> looks more logical and beautiful.
How is everything in C # 3?
The implementation of the field-like event described above corresponds to the C # 4 language (.NET 4.0). For earlier versions, there are very significant differences.
An implicit implementation uses lock (this) to provide thread safety instead of Interlocked.CompareExchange with a loop. For static events, lock (typeof (Class)) is used. Here is code similar to the compiler’s implicit event definition in C # 3:
classClass {
EventHandler changed;
publicevent EventHandler Changed {
add {
lock(this) { changed = changed + value; }
}
remove {
lock(this) { changed = changed - value; }
}
}
}
In addition, the work with the event inside the class is carried out as with a delegate, i.e. + = and - = call Delegate.Combine and Delegate.Remove directly, bypassing the add / remove methods. This change may make it impossible to build a project in C # 4! In C # 3, the result of + = and - = was a delegate, because The result of assigning a variable is always the assigned value. In C # 4, the result is void, because add / remove methods do not return values.
In addition to changes in the work on different versions of the language, there are several more features.
Feature # 1 - Subscriber Life Extension
When subscribing to an event, we add to the call list of the event delegate a reference to the method that will be called when the event is called. Thus, the memory occupied by the object subscribing to the event will not be freed until it is unsubscribed from the event or until the destruction of the object containing the event. This feature is one of the common causes of memory leaks in applications.
To correct this drawback, weak events are often used, weak events. This topic has already been covered on Habré .
Feature # 2 - explicit interface implementation
An event that is part of an interface cannot be implemented as a field when this interface is explicitly implemented. In such cases, you should either copy the standard event implementation for implementation as a property, or implement this part of the interface implicitly. Also, if you do not need the thread safety of this event, you can use the simplest and most effective definition:
EventHandler changed;
event EventHandler ISomeInterface.Changed {
add { changed += value; }
remove { changed -= value; }
}
Feature # 3 - Safe Call
Events before the call should be checked for null, which follows from the work of delegates described above. The code grows from this, to avoid which there are at least two ways. The first method is described by Jon Skeet in his book C # in depth :
publicevent EventHandler Changed = delegate { };
Short and concise. We initialize the event delegate with an empty method, so it will never be null. It is impossible to subtract this method from the delegate, because it is defined during the initialization of the delegate and it has neither a name nor a link to it from anywhere in the program.
The second way is to write a method that contains the necessary null check. This technique works especially well in .NET 3.5 and later, where extension methods are available. Since when calling the extension method, the object on which it is called is just a parameter of this method, this object can be an empty reference, which is used in this case.
publicstaticclassEventHandlerExtensions {
publicstaticvoidSafeRaise(this EventHandler handler, object sender, EventArgs e) {
if(handler != null)
handler(sender, e);
}
publicstaticvoid SafeRaise<TEventArgs>(this EventHandler<TEventArgs> handler,
object sender, TEventArgs e) where TEventArgs : EventArgs {
if(handler != null)
handler(sender, e);
}
}
Thus, we can trigger events like Changed.SafeRaise (this, EventArgs.Empty), which saves us lines of code. You can also define the third variant of the extension method for the case when we have EventArgs.Empty, so as not to pass them explicitly. Then the code will be reduced to Changed.SafeRaise (this), but I will not recommend this approach, because for other members of your team, this may not be as explicit as passing an empty argument.
Subtlety number 4 - what is wrong with the standard implementation?
If you have ReSharper, then you could watch his next message . The resolver team correctly believes that not all of your users are sufficiently informed about the work of events / delegates in terms of unsubscribing / subtraction, but nevertheless, your events should work predictably not for your users, but from the point of view of events in .NET, but because . there is such a feature, then it should remain in your code.
Bonus: Microsoft's attempt to make contra-events
In the first beta of C # 4, Microsoft tried to add contravariance to events. This allowed you to subscribe to the EventHandler <MyEventArgs> event using methods that have the EventHandler <EventArgs> signature and everything worked until several methods with a different (but suitable) signature were added to the event delegate. Such code compiled, but crashed with a runtime error. Apparently, they could not get around this and in the release of C # 4, contravariance for EventHandler was disabled.
This is not so noticeable if you omit the explicit creation of a delegate when subscribing, for example, the following code compiles fine:
publicclassTests {
publicevent EventHandler<MyEventArgs> Changed;
publicvoidTest() {
Changed += ChangedMyEventArgs;
Changed += ChangedEventArgs;
}
voidChangedMyEventArgs(object sender, MyEventArgs e) { }
voidChangedEventArgs(object sender, EventArgs e) { }
}
This is because the compiler itself will substitute new EventHandler <MyEventArgs> (...) to both subscriptions. If you use new EventHandler <EventArgs> (...) in at least one of the places, the compiler will report an error - it is impossible to convert the type of EventHandler <System.EventArgs> to EventHandler <Events.MyEventArgs> (here Events is the namespace of my test project).
Sources
The following is a list of sources, some of which were used in the preparation of the article. I recommend reading the book by Jon Skeet (Jon Skeet), which describes in detail not only the delegates, but also many other language tools.
- Jon Skeet. C # in Depth, Second Edition
- Chris Burrows. Events get a little overhaul in C # 4, Part I: Locks
- Chris Burrows. Events get a little overhaul in C # 4, Part II: Semantic Changes and + = / - =
- Chris Burrows. Events get a little overhaul in C # 4, Part III: Breaking Changes
- Chris Burrows. Events get a little overhaul in C # 4, Afterward: Effective Events
- MSDN 10.7 Events - Part of the C # Language Specification for .NET 1.1
- MSDN How to: Handle Multiple Events Using Event Properties
- JetBrains ReSharper. Delegate subtraction has unpredictable semantics
- StackOverflow Question. Event and delegate contravariance in .NET 4.0 and C # 4.0