⬆️ ⬇️

.NET events in detail

If you are a .NET programmer, then you must have announced 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 work of events in as much detail as possible, including some special cases that are rarely dealt with, but it is important and interesting to know about.



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 react to them.

Example of declaring an event:



public event EventHandler Changed; 


Consider what the ad consists of. First come the event modifiers, then the event keyword, after it the event type, which must be a delegate type, and the event identifier, that is, its name. The event keyword informs the compiler that this is not a public field, but a special way a drop-down construct that hides from the programmer the details of the implementation of the event mechanism. In order to understand how this mechanism works, it is necessary to study the principles of the work of delegates.



The core of the event is delegates.



We can say that the delegate in .NET is a kind of analogue of a function reference in C ++. However, such a definition is inaccurate, since Each delegate can refer not to one, but to an arbitrary number of methods that are stored in the delegate's call list (invocation list). 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 invoking the Invoke () method, a sequential call is made to each of the list methods. The delegate can be called as a function; the compiler translates such a call into an Invoke () call.

In C # for delegates, there are operators + and - that do not exist in the .NET environment and are 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 assignment operators (+ = and - =) is also applicable to delegate operators, as well as to the + and - operators defined in the .NET environment for other types. If, when subtracting from a delegate, its 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; Action c = a + b; Action d = a - b; a(); // A b(); // A c(); // AA d(); //  NullReferenceException, .. d == null 


Events - default implementation



Events in C # can be defined in two ways:
  1. Implicit implementation of an event (field-like event).
  2. Explicit implementation of the event.
I’ll clarify that the words “explicit” and “implicit” in this case are not terms defined in the specification, but simply describe the implementation method by meaning.



Consider the most commonly used event implementation — implicit. Suppose there is the following source code in C # 4 (this is important; for earlier versions, a slightly different code is generated, as will be discussed later):



 class Class { public event EventHandler Changed; } 


These lines will be translated by the compiler into a code similar to the following:



 class Class { EventHandler hanged; public event EventHandler Changed { add { EventHandler eventHandler = this.changed; EventHandler comparand; do { comparand = eventHandler; eventHandler = Interlocked.CompareExchange<EventHandler>(ref this.changed, comparand + value, comparand); } while(eventHandler != comparand); } remove { EventHandler eventHandler = this.changed; EventHandler comparand; do { comparand = eventHandler; eventHandler = Interlocked.CompareExchange<EventHandler>(ref this.changed, comparand - value, comparand); } while (eventHandler != comparand); } } } 


The add block is called when the event is subscribed, the remove block is called when unsubscribing. These blocks are compiled into separate methods with unique names. Both of these methods take one parameter — the delegate of the type that corresponds to the type of 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 trigger an event implemented implicitly from a class heir.



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 cycle is used for the case when, after assigning the variable delegate comparand to an event and before performing the comparison, another thread changes this delegate. In this case, Interlocked.CompareExchange does not replace, the boundary condition of the cycle is not met and the next attempt is made.



Declaration 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 via add / remove blocks, both of which must be present. Such an announcement is often used to create your own event mechanism while maintaining the convenience of the C # language in working with them.

For example, one of the typical implementations consists in a separate storage of the delegates' event dictionary, in which there are only those delegates whose events have been subscribed to. Access to the dictionary is carried out by keys, which are usually static fields of type object, used only to compare their links. This is done in order to reduce the amount of memory occupied by the class instance (in case it contains a large number of non-static events). This implementation is used in WinForms.



How do you subscribe to an event and call it?



All subscription and unsubscribe actions (denoted as + = and - = can be easily confused with delegate operators) are compiled into calls to the add and remove methods. Calls within the class other than the above are compiled into a simple work with the delegate. It should be noted that with the implicit (and with the right explicit) implementation of the event it is impossible to get access to the delegate from outside the class, you can only work with the event as with abstraction - by subscribing and unsubscribing. Since there is no way to determine whether we have subscribed to any event (if not to 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



Event scope modifiers can be used (public, protected, private, internal), they can be overridden (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 redefine it (as well as methods or properties with these two modifiers).



What types of events are there?



As already noted, the event type must always be a delegate type. The standard event types are EventHandler and EventHandler <TEventArgs>, where TEventArgs is the heir to EventArgs. The EventHandler type is used when event arguments are not provided, and the EventHandler <TEventArgs> type is used when event arguments are present, then a separate class is created for them - the heir from EventArgs. You can also use any other types of delegates, but using the typed EventHandler <TEventArgs> looks more logical and beautiful.



What is the situation in C # 3?



The implementation of the field-like event, which is described above, corresponds to the C # 4 (.NET 4.0) language. 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 a code similar to the compiler-opened implicit event definition in C # 3:



 class Class { EventHandler changed; public event EventHandler Changed { add { lock(this) { changed = changed + value; } } remove { lock(this) { changed = changed - value; } } } } 


In addition, work with an event inside the class is conducted as 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 the + = and - = was the delegate, since the result of the variable assignment is always the assigned value. In C # 4, the result is void, since add / remove methods do not return values.



In addition to changes in the work on different versions of the language there are a few more features.



Feature number 1 - the extension of the subscriber's lifetime



When you subscribe to an event, we add a reference to the method that will be called when the event is raised. Thus, the memory occupied by the object subscribing to the event will not be released until it unsubscribes the event or until the object containing the event is destroyed. This feature is one of the most common causes of memory leaks in applications.

To correct this deficiency often used weak events, weak events. This topic has already been covered on Habré .



Feature 2 is an explicit interface implementation.



An event that is part of an interface cannot be implemented as a field when the interface is explicitly implemented. In such cases, you should either copy the standard implementation of the event 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 - Secure Call



Events before calling should be checked for null, as follows from the work of delegates described above. From this grows the code, to avoid what there are at least two ways. The first method is described by Jon Skeet in his C # in depth book:



 public event EventHandler Changed = delegate { }; 


Short and concise. We initialize the event delegate with an empty method, so it will never be null. This method cannot be subtracted 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 test. This technique works especially well in .NET 3.5 and higher, where extension methods are available. Since when calling the extension method, the object on which it is called is only a parameter of this method, this object can be an empty reference, which is used in this case.



 public static class EventHandlerExtensions { public static void SafeRaise(this EventHandler handler, object sender, EventArgs e) { if(handler != null) handler(sender, e); } public static void 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 a 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, since for other members of your team, this may not be as obvious as passing an empty argument.



Subtlety # 4 - what's wrong with the standard implementation?



If you have a ReSharper, then you could watch the following message . The resharper team correctly believes that not all your users are sufficiently knowledgeable in the work of events \ delegates in terms of unsubscribe \ 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, it should remain in your code.



Bonus: Microsoft's attempt to make contravariant events



In the first beta of C # 4, Microsoft tried to add contra-variant to the events. This allowed you to subscribe to an EventHandler <MyEventArgs> event with methods that have an EventHandler <EventArgs> signature and everything worked until several methods with a different (but suitable) signature were added to the event delegate. Such code was compiled, but fell with a runtime error. Apparently, they could not get around this and in the release of C # 4 contravariant for EventHandler was disabled.

This is not so noticeable if you omit the explicit creation of the delegate upon subscription, for example, the following code will compile perfectly:



 public class Tests { public event EventHandler<MyEventArgs> Changed; public void Test() { Changed += ChangedMyEventArgs; Changed += ChangedEventArgs; } void ChangedMyEventArgs(object sender, MyEventArgs e) { } void ChangedEventArgs(object sender, EventArgs e) { } } 


This is because the compiler itself will substitute the new EventHandler <MyEventArgs> (...) to both subscriptions. If at least in one of the places to use new EventHandler <EventArgs> (...), the compiler will report an error - it is not possible to convert the EventHandler <System.EventArgs> type into EventHandler <Events.MyEventArgs> (here Events is the namespace of my test project).



Sources



The following is a list of sources, part of the material from which was used in the preparation of the article. I recommend reading the book by John Skit (Jon Skeet), in which not only delegates, but many other means of language are described in detail.

Source: https://habr.com/ru/post/148562/



All Articles