Delegates and Events in .NET

Original author: Jon Skeet
  • Transfer
From the translator. Judging from my own experience, as well as from the experience of my fellow programmers, I can say that among all the basic functions of the C # language and the .NET platform, delegates and events are one of the most complex. Perhaps this is due to the fact that the need for delegates and events at first glance seems unobvious, or because of some confusion in terms. So I decided to translate an article by John Skeet about delegates and events at the most basic level, “on the fingers”. It is ideal for those who are familiar with C # /.NET, but have difficulty understanding delegates and events.

The translation presented here is free. However, if “free”, as a rule, is understood as an abbreviated translation, with omissions, simplifications and paraphrases, then everything is the other way around. This translation is a slightly expanded, refined and updated version of the original. I express my deep gratitude to Sergey Teplyakov aka SergeyT , who made an invaluable contribution to the translation and design of this article.


People often have difficulty understanding the differences between events and delegates. And C # confuses the situation even more, because it allows you to declare field-like events that are automatically converted to a delegate variable with the same name. This article is intended to clarify this issue. Another point is the confusion with the term “delegate”, which has several meanings. Sometimes it is used to indicate the type (delegate type) delegate, and sometimes - to refer to an instance of the delegate (delegate instance). To avoid confusion, I will explicitly use these terms — the type of delegate and the instance of the delegate, and when I use the word “delegate” - that means I am talking about them in the broadest sense.

Delegate Types


In a sense, you can think of the delegate type as an interface in which only one method with a clearly defined signature is defined (in this article, by the signature of the method I will understand all its input and output (ref and out) parameters, as well as return value). Then the delegate instance is an object that implements this interface. In this understanding, having a delegate instance, you can call any existing method whose signature will match the signature of the method defined in the "interface". Delegates also have other functionality, but the ability to make method calls with predefined signatures is the very essence of delegates. The delegate instance stores a reference (pointer, label) to the target method and, if this method is an instance, then a reference to the instance of the object (class or structure),

The delegate type is declared using the keyword delegate. Delegate types can exist as independent entities, or be declared inside classes or structures. For instance:

namespace DelegateArticle
 {
     public delegate string FirstDelegate (int x);
     public class Sample
     {
         public delegate void SecondDelegate (char a, char b);
     }
 }

In this example, two types of delegate are declared. The first is DelegateArticle.FirstDelegateone that is declared at the namespace level. It is "compatible" with any method that has one type parameter intand returns a type value string. The second one DelegateArticle.Sample.SecondDelegate, which is already declared inside the class and is its member. It is "compatible" with any method that has two type parameters charand does not return anything, since the return type is marked as void.

Note that both types of delegate have an access modifierpublic. In general, with respect to access modifiers, delegate types behave the same as classes and structures. If the access modifier is not explicitly specified for the delegate type and this type is declared inside the namespace, then it will be available for all objects that are also inside this namespace. If the delegate type without the modifier is declared inside the class or structure, then it will be closed, similar to the action of the modifier private.

When declaring a delegate type, you cannot use a modifier static.

But remember that a keyword delegatedoes not always mean a delegate type declaration. The same keyword is used when creating delegate instances using anonymous methods.

Both types of delegate declared in this example inherit fromSystem.MulticastDelegate, which, in turn, is inherited from System.Delegate. In practice, consider inheritance only from MulticastDelegate- the difference between Delegateand MulticastDelegatelies primarily in the historical aspect. These differences were significant in beta versions of .NET 1.0, but it was inconvenient, and Microsoft decided to combine the two types into one. Unfortunately, the decision was made too late, and when it was made, they did not dare to make such a serious change affecting the foundation of .NET. Therefore, consider that Delegateand MulticastDelegateis the same thing.

Each delegate type that you create inherits members from MulticastDelegate, namely one constructor with parameters Objectand IntPtr, as well as the three methods Invoke, BeginInvokeandEndInvoke. We will return to the constructor a little later. In fact, these three methods are not literally inherited, since their signature for each type of delegate is different - it "adapts" to the signature of the method in the declared delegate type. Looking at the sample code above, we derive the “inherited” methods for the first type of delegate FirstDelegate:

public string Invoke (int x);
public System.IAsyncResult BeginInvoke(int x, System.AsyncCallback callback, object state);
public string EndInvoke(IAsyncResult result);

As you can see, the return type of the methods Invokeand EndInvokethe same as those specified in the signature of the delegate, as well as the method parameter Invokeand the first parameter BeginInvoke. We will look at the purpose of the method Invokelater in the article, BeginInvokeand EndInvokewe will look at the section that describes the advanced use of delegates . It’s too early to talk about this since we don’t even know how to create delegate instances. We’ll talk about this in the next section.

Delegate Instances: The Basics


Now we know how the delegate type is declared and what it contains, so let's take a look at how you can instantiate a delegate and what can be done with it.

Creating Delegate Instances

First of all, I note that this article does not talk about the new functionality of C # 2.0 and 3.0 related to creating delegate instances, just as it does not cover the generalized delegates that appeared in C # 4.0. My separate article on closures, The Beauty of Closures , talks about the new delegate features that appeared in C # 2.0 and 3.0; moreover, a lot of information on this topic is contained in chapters 5, 9 and 13 of my book “ C # in Depth". I will adhere to the explicit delegate instantiation style that appeared in C # 1.0 / 1.1, as I believe that such a style is easier to understand what is happening “under the hood”. So when you understand the basics, you can begin to learn new features from C # 2.0, 3.0, and 4.0; and vice versa, without a solid understanding of the fundamentals set forth in this article, the “new” functionality of delegates may be unbearable for you.

As mentioned earlier, each delegate instance necessarily contains a reference to the target method, which can be called through this delegate instance, and a reference to the instance of the object (class or structure) in which the target method is declared. If the target method is static, then of course there is no reference to the instance. The CLR also supports other, slightly different forms of delegates, where the first argument passed to the static method is stored in the delegate instance, or the reference to the target instance method is passed as an argument when the method is called. You can read more about this System.Delegatein the MSDN documentation , but now, at this stage, this additional information is not significant.

So, we know that to create an instance we need two “units” of data (well, the delegate type itself, of course), but how do you let the compiler know about it? We use what the C # specification calls “ delegate-creation-expression ”, which is a form of new delegate-type (expression). An expression must be either another delegate with the same type (or a compatible delegate type in C # 2.0), or a “method group” that consists of a method name and an optional reference to an instance of the object. A group of methods is specified just like a regular method call, but without any arguments and parentheses. The need to create copies of a delegate is quite rare, so we will focus on more general forms. Examples are below.

/* Два выражения создания экземпляров делегатов d1 и d2 эквивалентны. Здесь InstanceMethod является экземплярным методом, который объявлен в классе, в котором также объявлены нижеприведённые выражения (базовый класс). Соответственно, ссылка на экземпляр объекта — this, и именно поэтому эти выражения эквивалентны. */
FirstDelegate d1 = new FirstDelegate(InstanceMethod);
 FirstDelegate d2 = new FirstDelegate(this.InstanceMethod);
/* Здесь (d3) мы создаём экземпляр делегата, ссылающийся на тот же метод, что и в предыдущих двух выражениях, но на этот раз с другим экземпляром класса. */
FirstDelegate d3 = new FirstDelegate(anotherInstance.InstanceMethod);
/* В этом (d4) экземпляре делегата используется уже другой метод, тоже экземплярный, который объявлен в другом классе; мы указываем экземпляр этого класса и сам метод. */
FirstDelegate d4 = new FirstDelegate(instanceOfOtherClass.OtherInstanceMethod);
/* А вот этот (d5) экземпляр делегата использует статический метод, который расположен в том же классе, где и это выражение (базовом классе). */
FirstDelegate d5 = new FirstDelegate(StaticMethod);
/* Здесь (d6) экземпляр делегата использует другой статический метод, объявленный на этот раз в стороннем классе. */
FirstDelegate d6 = new FirstDelegate(OtherClass.OtherStaticMethod);

The delegate constructor that we talked about earlier has two parameters — a reference to the type method being called System.IntPtr(this parameter is called method in the MSDN documentation) and a reference to an instance of the type object System.Object(this parameter is called target in the MSDN documentation), which is null if the method specified in the method parameter is static.

An important point to make: delegate instances can refer to methods and instances of objects that are invisible (out of scope) with respect to the place in the code where the call will be madedelegate instance. For example, when creating a delegate instance, a private method can be used, and then this delegate instance can be returned from another public method or property. On the other hand, the instance of the object specified when creating the instance of the delegate may be an object that, when called, will be unknown with respect to the object in which the call was made. The important thing is that both the method and the instance of the object must be accessible (in scope) at the time of creationdelegate instance. In other words, if (and only if) in the code you can create an instance of a certain object and call a specific method from this instance, then you can use this method and an instance of the object to create an instance of the delegate. But during a call to a previously created instance of a delegate, access rights and scope are ignored. Speaking of challenges ...

Invoking Delegate Instances

Delegate instances are invoked in the same way as regular methods are invoked. For example, a call to the delegate instance d1, whose type is defined at the very top as delegate string FirstDelegate (int x), will be as follows:
string result = d1(10);

The method referenced by the delegate instance is called “within” (or “in context”, in other words) the object instance, if any, and the result is returned. Writing a full-fledged program that demonstrates the work of delegates, and at the same time compact, not containing "extra" code, is a difficult task. However, the following is a similar program containing one static and one instance method. A DelegateTest.StaticMethodcall StaticMethodis equivalent to a call - I included the class name to make the example more understandable.

using System;
public delegate string FirstDelegate (int x);
class DelegateTest
 {    
     string name;
     static void Main()
     {
         FirstDelegate d1 = new FirstDelegate(DelegateTest.StaticMethod);
         DelegateTest instance = new DelegateTest();
         instance.name = "My instance";
         FirstDelegate d2 = new FirstDelegate(instance.InstanceMethod);
         Console.WriteLine (d1(10)); // Выводит на консоль "Static method: 10"
         Console.WriteLine (d2(5));  // Выводит на консоль "My instance: 5"
     }
     static string StaticMethod (int i)
     {
         return string.Format ("Static method: {0}", i);
     }
     string InstanceMethod (int i)
     {
         return string.Format ("{0}: {1}", name, i);
     }
 }

The C # syntax for invoking delegate instances is syntactic sugar that masks the invocation of the method Invokethat each delegate type has. Delegates can run asynchronously if they provide methods BeginInvoke/EndInvoke, but more on that later .

Delegate Combination

Delegates can be combined (combined and subtracted) in such a way that when you call one instance of a delegate, a whole set of methods is called, and these methods can be from different instances of different classes. When I said earlier that a delegate instance stores references to a method and an instance of an object, I simplified things a bit. This is true for delegate instances that represent the same method. For clarity, hereinafter I will call such instances of delegates “simple delegates” (simple delegate). In contrast, there are delegate instances that are actually lists of simple delegates, all of which are based on the same type of delegate (i.e. they have the same signature of the methods referenced). Such delegate instancesI will call “combined delegates”. Several combined delegates can be combined with each other, effectively becoming one big list of simple delegates. The list of simple delegates in the combined delegate is called the “call list” or “invocation list”. Thus, a call list is a list of pairs of links to methods and instances of objects that (pairs) are located in the order of the call.

It is important to know that delegate instances are always immutable. Each time when combining instances of delegates (as well as when subtracting - we will consider this below), a new combined delegate is created. Exactly as with strings: if you applyString.PadLeft to the string instance, the method does not change this instance, but returns a new instance with the changes made.

Combining (the term “addition” also occurs) between two delegate instances is usually done using the addition operator, as if the delegate instances were numbers or strings. Similarly, the subtraction (the term “delete” is also found) of one instance of a delegate from another is performed using the subtraction operator. Keep in mind that when subtracting one combined delegate from another, the subtraction is performed as part of the call list. If in the original (reduced) call list there is not one of those simple delegates who are in the deductible call list, then the result (the difference) will be the original list. Otherwise, if in the original list there are simple delegates present in the subtracted one, then only the last ones will be absent in the resulting listoccurrences of simple delegates. However, it is easier to show with examples than to describe in words. But instead of the next source code, I will demonstrate the operation of combining and subtraction using the example of the following table. In it, literals d1, d2, d3 denote simple delegates. Further, the designation [d1, d2, d3] means a combined delegate, which consists of three simple ones in this order, i.e. when called, d1 will be called first, then d2, and then d3. An empty call list is null.
ExpressionResult
null + d1d1
d1 + nulld1
d1 + d2[d1, d2]
d1 + [d2, d3][d1, d2, d3]
[d1, d2] + [d2, d3][d1, d2, d2, d3]
[d1, d2] - d1d2
[d1, d2] - d2d1
[d1, d2, d1] - d1[d1, d2]
[d1, d2, d3] - [d1, d2]d3
[d1, d2, d3] - [d2, d1][d1, d2, d3]
[d1, d2, d3, d1, d2] - [d1, d2][d1, d2, d3]
[d1, d2] - [d1, d2]null

In addition to the addition operator, delegate instances can be combined using the static method Delegate.Combine; similarly, the subtraction operation has an alternative in the form of a static method Delegate.Remove. Generally speaking, the addition and subtraction operators are a kind of syntactic sugar, and the C # compiler, meeting them in the code, replaces it with calls to the Combine and Remove methods. And precisely because these methods are static, they easily cope with null-instances of delegates.

The addition and subtraction operators always work as part of the assignment operation d1 += d2, which is completely equivalent to the expressiond1 = d1+d2; same for subtraction. Again, I remind you that delegate instances involved in addition and subtraction do not change during the operation; in this example, the variable d1 will simply change the link to the newly created combined delegate, consisting of the “old” d1 and d2.

Please note that adding and removing delegates occurs from the end of the list, therefore the sequence of calls x + = y; x - = y; equivalent to an empty operation (the variable x will contain an unchanged list of subscribers, approx. transl. ).

If the delegate type signature is declared such that it returns a value (that is, the return value is not void) and a “combined” instance of the delegate is created based on this type, then when it is called, the return value “provided” by the last simple delegate will be written into the variable in the combined delegate call list.

If there is a combined delegate (containing a call list consisting of many simple delegates), and when it is called in some simple delegate, an exception occurs, then at this point the call of the combined delegate will stop, the exception will be thrown, and all other simple delegates from the call list never will be called.

Events


First things first: events are not delegate instances. And now again:
Events are NOT delegate instances.

In a sense, it is unfortunate that the C # language allows the use of events and delegate instances in certain situations in the same way, but it is very important to understand the difference.

I came to the conclusion that the best way to understand events is to think of them as “like” properties. Properties, although they look “kind of like” fields, are definitely not really them - you can create properties that don't use fields at all. Events behave in a similar manner - although they look like delegate instances in terms of addition and subtraction operations, they are not really them.

Events are pairs of methods that are appropriately “framed” in IL (CIL, MSIL) and interconnected so that the language environment clearly knows that it “deals” not with “simple” methods, but with methods that represent events. The methods correspond to the operations add (add) and remove(remove), each of which takes one parameter with a delegate instance that has a type that is the same as the type of event. What you will do with these operations is largely up to you, but usually the add and remove operations are used to add and remove delegate instances to / from the list of event handlers. When an event is triggered (and it doesn’t matter what caused the triggering - a click on a button, a timer, or an unhandled exception), handlers are called in turn (one after the other). Be aware that in C #, calling event handlers is not part of the event itself.

The methods of adding and removing are called in C # in one way eventName += delegateInstance;or another eventName -= delegateInstance;, where it eventNamecan be indicated by reference to an instance of an object (for example,myForm.Click) or by type name (e.g. MyClass.SomeEvent). However, static events are quite rare.

Events themselves can be declared in two ways. The first way is with an explicit implementation of the add and remove methods; this method is very similar to properties with explicitly declared getters and setters (but) with the keyword event. The following is an example property for a delegate type System.EventHandler. Note that in the add and remove methods, there is no operation with delegate instances that are passed there - the methods simply print messages to the console that they were called. If you execute this code, you will see that the remove method will be called, despite the fact that we passed null to it for removal.

using System;
class Test
 {
     public event EventHandler MyEvent //публичное событие MyEvent с типом EventHandler
     {
         add
         {
             Console.WriteLine ("add operation");
         }
         remove
         {
             Console.WriteLine ("remove operation");
         }
     }       
     static void Main()
     {
         Test t = new Test();
         t.MyEvent += new EventHandler (t.DoNothing);
         t.MyEvent -= null;
     }
    //Метод-заглушка, сигнатура которого совпадает с сигнатурой типа делегата EventHandler
     void DoNothing (object sender, EventArgs e)
     {
     }
 }

The moments when it is necessary to ignore the obtained value valuearise rather rarely. And although the cases in which we can ignore the value passed in this way are extremely rare, there are times when the use of a simple delegate variable for keeping subscribers is not suitable for us. For example, if a class contains many events, but subscribers will use only some of them, we can create an associative array, the event description will be used as the key, and the delegate with its subscribers will be used as the value. It is this technique that is used in Windows Forms - i.e. a class can contain a huge number of events without wasting memory for variables, which in most cases will be null.

Field-like events


C # provides an easy way to declare a delegate variable and an event at the same time. This method is called a "field-like event" and is declared very simply - just like the "long" form for declaring an event (shown above), but without the "body" with the add and remove methods.
public event EventHandler MyEvent;

This form creates a delegate variable and an event of the same type. Access to the event is determined in the event declaration using the access modifier (that is, in the example above, a public event is created), but the delegate variable is always private. The implicit body of the event is deployed by the compiler to the very obvious operations of adding and removing delegate instances to / from the delegate variable, and these actions are performed under a lock. For C # 1.1, the event MyEventfrom the example above is equivalent to the following code:

private EventHandler _myEvent;
public event EventHandler MyEvent
 {
     add
     {
         lock (this)
         {
             _myEvent += value;
         }
     }
     remove
     {
         lock (this)
         {
             _myEvent -= value;
         }
     }        
 }

This is for instance members. As for static events, the variable is also static and the lock is captured on the type of viewtypeof(XXX)where XXX is the name of the class in which the static event is declared. C # 2.0 makes no guarantees what is used to capture locks. It only says that to block instance events, the only object associated with the current instance is used, and to block static events, the only object associated with the current class is used. (Note that this is true only for events declared in classes, but not in structures. There are problems with event locking in structures; and in practice, I don’t remember a single example of the structure in which the event was declared.) But none of this is not as useful as you might think, see the multithreading section for details .

So what happens when you refer toMyEvent? Inside the body of the type itself (including nested types), the compiler generates code that references the delegate variable ( _myEventin the example above). In all other contexts, the compiler generates code that refers to the event.

What's the point of this?


Now that we know about the delegates and the events, a completely logical question arises: why do we need both in the language? The answer is because of encapsulation. Suppose that in some fictional C # /. NET events do not exist. How then can a third-party class subscribe to an event? Three options:
  1. Public variable (field) with the delegate type.
  2. A private variable (field) with a delegate type with a wrapper in the form of a public property.
  3. Private variable (field) with delegate type with public methods AddXXXHandler and RemoveXXXHandler.

Option number 1 is terrible - we usually hate public fields. Option # 2 is slightly better, but allows subscribers to efficiently override one of one - it will be too easy to write an expression someInstance.MyEvent = eventHandler;, as a result of which all existing handlers will be replaced with eventHandler, instead of adding to existing ones eventHandler. Plus, you still need to explicitly write the property code.

Option number 3 is, in fact, what events provide you with, but with a guaranteed agreement (generated by the compiler and reserved special flags in IL) and with a “free” implementation, if you are happy with the semantics of field-like events. Subscribing and unsubscribing to / from events is encapsulated without providing random access to the list of event handlers, which makes it possible to simplify the code for subscription and unsubscribe operations.

Thread safe events


(Note: with the release of C # 4, this section is somewhat outdated. From the translator: for more details, see the section “ From the translator ”)

Earlier, we touched on locking (locking), which occurs in field-like events during add and remove operations, which are automatically implemented by the compiler . This is done to provide some kind of thread safety guarantee. Unfortunately, it is not so useful. First of all, even in C # 2.0, specifications allow you to set a lock on a link to this object or to the type itself in static events. This contradicts the principles of locking on private links, which is necessary to prevent deadlocks.

Ironically, the second problem is the exact opposite of the first - due to the fact that in C # 2.0 you cannot guarantee which lock will be used, you yourself cannot use it when you trigger an event to make sure you see the newest ( actual) value in this stream. You can use the lock on something else or use special methods that work with memory barriers, but all this leaves an unpleasant aftertaste 1 ↓ .

If you want your code to be truly thread-safe, such that when you raise an event, you always use the most relevant value of the delegate variable, and also so that you can make sure that the add / remove operations do not interfere with one another, then to achieve of such "reinforced concrete" thread safety you need to write the body of add / remove operations yourself. Example below:

/// 
/// Переменная типа делегата SomeEventHandler, являющаяся «фундаментом» события.
/// 
 SomeEventHandler someEvent;
/// 
/// Примитив блокировки для доступа к событию SomeEvent.
/// 
readonly object someEventLock = new object();
/// 
/// Само событие
/// 
public event SomeEventHandler SomeEvent
 {
     add
     {
         lock (someEventLock)
         {
             someEvent += value;
         }
     }
     remove
     {
         lock (someEventLock)
         {
             someEvent -= value;
         }
     }
 }
/// 
/// Вызов события SomeEvent
/// 
protected virtual void OnSomeEvent(EventArgs e)
 {
     SomeEventHandler handler;
     lock (someEventLock)
     {
         handler = someEvent;
     }
     if (handler != null)
     {
         handler (this, e);
     }
 }

You can use a single lock for all your events, and even use this lock for anything else - it already depends on the specific situation. Please note that you need to "write" the current value to a local variable inside the lock (in order to get the most current value), and then check this value for null and do it outside the lock: holding the lock during the event call is a very bad idea, easily deadlockable. To explain this, imagine that a certain event handler should wait for another thread to do some kind of work, and if this other thread calls the add / remove operation for your event, you will get a deadlock.

The above code works correctly because once the local variable handleris set to someEvent, the value of handler will not change even if it changes itself someEvent. If all event handlers unsubscribe from the event, the call list will be empty, it someEventwill become null, but it handlerwill keep its value, which will be what it was at the time of the assignment. In fact, delegate instances are immutable, so any subscribers who sign up between an assignment ( handler = someEvent) and an event ( handler (this, e);) call will be ignored.

In addition, you need to determine if you need thread safety at all. Are you going to add and remove event handlers from other threads? Are you going to trigger events from different threads? If you are in full control of your application, then the answer is “no” very correct and easy to implement. If you are writing a class library, then most likely, thread safety will come in handy. If you definitely don’t need thread safety, then it’s a good idea to implement the body of add / remove operations yourself so that they do not explicitly use locks; because, as we recall, C # uses “its own” “wrong” locking mechanism when auto-generating these operations. In this case, your task is very simple. Below is an example of the above code, but without thread safety.

/// 
/// Переменная типа делегата SomeEventHandler, являющаяся «фундаментом» события.
/// 
 SomeEventHandler someEvent;
/// 
/// Само событие
/// 
public event SomeEventHandler SomeEvent
 {
     add
     {
         someEvent += value;
     }
     remove
     {
         someEvent -= value;
     }
 }
/// 
/// Вызов события SomeEvent
/// 
protected virtual void OnSomeEvent(EventArgs e)
 {
     if (someEvent != null)
     {
         someEvent (this, e);
     }
 }

If at the time of the method call, the OnSomeEventdelegate variable someEventdoes not contain a list of delegate instances (due to the fact that they were not added through the add method or were deleted through the remove method), then the value of this variable will be null, and to avoid its call with such a value, and a null check has been added. A similar situation can be solved in another way. You can create an instance of a no-op delegate that will be bound to the default variable and will not be deleted. In this case, in the method OnSomeEventyou just need to get and call the value of the delegate variable. If the “real” instances of the delegates have not been added, then a stub will be simply called.

Delegate Instances: Other Methods


Earlier in the article, I showed that a call someDelegate(10)is just a shorthand for a call someDelegate.Invoke(10). In addition Invoke, delegate types also have asynchronous behavior through a pair of BeginInvoke/ methods EndInvoke. In the CLI, they are optional, but in C # they are always there. They adhere to the same asynchronous execution model as the rest of .NET, allowing you to specify a callback handler along with an object that stores state information. As a result of an asynchronous call, the code is executed in threads created by the system and located in the thread pool pool of .NET.

In the first example, below 2 ↓ , there are no callbacks, they are simply used here BeginInvokeandEndInvokein one thread. Such a code template is sometimes useful when one thread is used for operations synchronous in general, but at the same time it contains elements that can be executed in parallel. For the sake of simplicity of code, all the methods in the example are static, but of course you can use the “asynchronous” delegates along with the instance methods, and in practice this will happen even more often. The method EndInvokereturns the value that is returned as a result of invoking the delegate instance. If an exception occurs during the invocation of the delegate instance, that exception will throw and EndInvoke.

using System;
using System.Threading;
delegate int SampleDelegate(string data);
class AsyncDelegateExample1
{
     static void Main()
     {
          SampleDelegate counter = new SampleDelegate(CountCharacters);
          SampleDelegate parser = new SampleDelegate(Parse);
          IAsyncResult counterResult = counter.BeginInvoke("hello", null, null);
          IAsyncResult parserResult = parser.BeginInvoke("10", null, null);
          Console.WriteLine("Основной поток с  ID = {0} продолжает выполняться",
               Thread.CurrentThread.ManagedThreadId);
          Console.WriteLine("Счётчик вернул '{0}'", counter.EndInvoke(counterResult));
          Console.WriteLine("Парсер вернул '{0}'", parser.EndInvoke(parserResult));
          Console.WriteLine("Основной поток с  ID = {0} завершился",
               Thread.CurrentThread.ManagedThreadId);
     }
     static int CountCharacters(string text)
     {
          Thread.Sleep(2000);
          Console.WriteLine("Подсчёт символов в строке '{0}' в потоке с ID = {1}",
               text, Thread.CurrentThread.ManagedThreadId);
          return text.Length;
     }
     static int Parse(string text)
     {
          Thread.Sleep(100);
          Console.WriteLine("Парсинг строки '{0}' в потоке с ID = {1}",
               text, Thread.CurrentThread.ManagedThreadId);
          return int.Parse(text);
     }
}

Method calls Thread.Sleepare inserted only in order to demonstrate that the methods CountCharactersand Parseactually carried out in parallel with the main flow. A CountCharacters2-second sleep is large enough to force the thread pool to perform tasks on other threads - the thread pool serializes requests that do not take a lot of time to complete, thus avoiding the excessive creation of new threads (creating new threads is a relatively resource-intensive operation) . By “lulling” the flow for a long time, we thus simulate a “heavy”, time-consuming task. And here is the conclusion of our program:

	The main thread with ID = 9 continues to run
	Parsing string '10' in stream with ID = 10
	Counting characters in the string 'hello' in a stream with ID = 6
	Counter returned '5'
	Parser returned '10'
	The main thread with ID = 9 ended

If the delegate execution process in a third-party thread has not yet completed, then a method call EndInvokein the main thread will have a similar effect to a call Thread.Joinfor manually created threads - the main thread will wait until the task in the third-party thread completes. The value IAsyncResultthat is returned by the method BeginInvokeand passed to the input EndInvokecan be used to transfer state from BeginInvoke(via the last parameter - Object state), however, the need for such a transfer when using delegates does not occur often.

The code above is pretty simple, but not efficient enough compared to the callback model called after the delegate completes. As a rule, it is in the callback method that the method is calledEndInvokereturning the result of the delegate. Although, purely theoretically, this call still blocks the main thread (as in the above example), but in practice the thread will not be blocked, since the callback method is executed only when the delegate has completed its task. The callback method can use a state with some additional data, which will be passed to it from the method BeginInvoke. The example below uses the same delegates to parse and count the number of characters in a string as in the example above, but this time with a callback method in which the result is displayed on the console. The statetype parameter is Objectused to transmit information about in what format to output the result to the console, and thanks to this we can use one callback methodDisplayResultto handle both asynchronous delegate calls. Pay attention to the conversion of type IAsyncResultto type AsyncResult: the value accepted by the callback method is always an instance AsyncResult, and through it we can get the original delegate instance, the result of which we will get using EndInvoke. It’s a little strange here that the type is AsyncResultdeclared in the namespace System.Runtime.Remoting.Messaging(which you need to connect), while all other types are declared in Systemor System.Threading.

using System;
using System.Threading;
using System.Runtime.Remoting.Messaging;
delegate int SampleDelegate(string data);
class AsyncDelegateExample2
{
     static void Main()
     {
          SampleDelegate counter = new SampleDelegate(CountCharacters);
          SampleDelegate parser = new SampleDelegate(Parse);
          AsyncCallback callback = new AsyncCallback(DisplayResult);
          counter.BeginInvoke("hello", callback, "Счётчик вернул '{0}' в потоке с ID = {1}");
          parser.BeginInvoke("10", callback, "Парсер вернул '{0}' в потоке с ID = {1}");
          Console.WriteLine("Основной поток с  ID = {0} продолжает выполняться", 
               Thread.CurrentThread.ManagedThreadId);
          Thread.Sleep(3000);
          Console.WriteLine("Основной поток с  ID = {0} завершился", 
               Thread.CurrentThread.ManagedThreadId); 
     }
     static void DisplayResult(IAsyncResult result)
     {
          string format = (string)result.AsyncState;
          AsyncResult delegateResult = (AsyncResult)result;
          SampleDelegate delegateInstance = 
               (SampleDelegate)delegateResult.AsyncDelegate;        
          Int32 methodResult = delegateInstance.EndInvoke(result);
          Console.WriteLine(format, methodResult, Thread.CurrentThread.ManagedThreadId);
     }
     static int CountCharacters(string text)
     {
          Thread.Sleep(2000);
          Console.WriteLine("Подсчёт символов в строке '{0}' в потоке с ID = {1}", 
               text, Thread.CurrentThread.ManagedThreadId);
          return text.Length;
     }
     static int Parse(string text)
     {
          Thread.Sleep(100);
          Console.WriteLine("Парсинг строки '{0}' в потоке с ID = {1}", 
               text, Thread.CurrentThread.ManagedThreadId);
          return int.Parse(text);
     }
}

This time, almost all the work is done in threads from the thread pool. The main thread simply initiates asynchronous tasks and “falls asleep” until all these tasks are completed. All threads from the thread pool are background threads that cannot "hold" the application (that is, they cannot prevent it from closing), and so that the application does not end before the delegates work in the background threads, we applied the call Thread.Sleep(3000)in the main thread - you can hope that 3 seconds is enough to complete and complete the delegates. You can verify this by commenting out the line Thread.Sleep(3000)- the program will end almost instantly after starting.

The result of our program is presented below. Pay attention to the order of outputting results to the console - the result of the parser appeared before the result of the counter, since the environment does not guarantee the preservation of order when called EndInvoke. In the previous example, parsing was completed much faster (100 ms) than the counter (2 sec), however, the main thread was waiting for both of them to display primarily the result of the counter, and only then the parser.

	The main thread with ID = 9 continues to run
	Parsing string '10' in stream with ID = 11
	The parser returned '10' in the stream with ID = 11
	Counting characters in the string 'hello' in a stream with ID = 10
	The counter returned '5' in the stream with ID = 10
	The main thread with ID = 9 ended

Remember that when using this asynchronous model you must call EndInvoke; this is necessary in order to guarantee the absence of memory leaks and handlers. In some cases, in the absence of EndInvokeleaks, it may not be, but one should not hope for it. For more detailed information, you can refer to my article “ Multi-threading in .NET: Introduction and suggestions ”, devoted to multithreading, in particular, to the section “ The Thread Pool and Asynchronous Methods ”.

Conclusion


Delegates provide an easy way to call methods taking into account instances of objects and with the ability to transfer certain data. They are the basis for events, which in themselves are an effective mechanism for adding and removing handlers that will be called at the appropriate time.

Notes


1. Note perev. I admit, John Skeet's explanation is rather slurred and crumpled. To understand in detail why blocking on the current instance and type is bad, and why a separate read-only private field should be entered, I highly recommend using the book “CLR via C #” by Jeffrey Richter, who has already survived 4 editions. If we talk about the second edition of 2006, translated into Russian in 2007, the information on this problem is located in “Part 5. CLR tools” - “Chapter 24. Stream synchronization” - section “Why did the“ excellent ”idea turn out to be so unsuccessful ".

2. Note perev. This and the following code examples, as well as their output to the console, have been slightly changed compared to the original examples from J. Skeet. In addition to the translation, I added the output of thread identifiers so that it is clearly visible which code is executed in which thread.


From translator


Despite the rather large size of the article, one cannot but agree that the topic of delegates and events is much more extensive, complex and multifaceted. However, a hypothetical article fully describing delegates and events would have a size similar to the size of an average book. Therefore, I provide links to the most useful articles on the topic, and those that complement this article as harmoniously as possible.

Alexey Dubovtsev. Delegates and Events (RSDN).
Although the article is not new (dated 2006) and considers only the basics of delegates and events, the level of “consideration of the basics” is much deeper: here is a closer look at the MulticastDelegate type, especially in terms of combined delegates, and a description of the principle of operation at the MSIL level, and a description of the class EventHandlerList, and more. In general, if you want to consider the basics of delegates and events at a deeper level, then this article is definitely for you.

coffeecupwinner . .NET events in detail .
I hope you noticed a note on outdated material at the beginning of the thread safe events section.? In C # 4, the internal implementation of field-like events criticized by Skeet and Richter has been completely redesigned: now thread safety is implemented through Interlocked.CompareExchange, without any locks. This, among other things, is described in this article. In general, the article scrupulously considers only events, but at a much deeper level than that of John Skeet.

Daniel Grunwald. andreycha . Weak events in C # .
When talking about the advantages between C # /. NET on the one hand and C ++ on the other, the first to write, among other things, is automatic garbage collection, which eliminates memory leaks as a class of errors. However, not everything is so rosy: events can lead to memory leaks, and this very detailed article is devoted to solving these problems.

rroyter . Why do I need delegates in C #?
As I mentioned in the introduction, in novice developers, misunderstandings with delegates are related to the lack of visible reasons requiring their use. This article very clearly demonstrates some situations where delegates will be extremely appropriate. In addition, the new delegate features introduced in C # 2nd and 3rd versions are demonstrated here.

Also popular now: