Delegates and events in C #

Keywords: C# WPF

introduction

Delegates and events are widely used in the. Net Framework. However, a better understanding of delegates and events is not easy for many people who have not been in contact C# for a long time. They are like a threshold. People who pass this threshold feel it's too easy. People without the past feel uneasy and uncomfortable every time they see entrustment and events. In this article, I will use two examples to explain what is a delegate, why to use a delegate, the origin of events, the delegates and events in the. Net Framework, the significance of delegates and events to the Observer design pattern, and their intermediate codes.

Take the method as an argument to the method

Let's take a look at the following two simplest methods, no matter how detour the title is, and no matter what the entrustment is, they just output a greeting on the screen:

public void GreetPeople(string name) {
    // Do some extra things, such as initialization, which is omitted here
    EnglishGreeting(name);
}
public void EnglishGreeting(string name) {
    Console.WriteLine("Morning, " + name);
}

Whether these two methods have any practical significance or not. GreetPeople is used to say hello to someone. When we pass the name parameter representing someone's name, such as "Jimmy", in this method, we will call the EnglishGreeting method and pass the name parameter again. EnglishGreeting is used to output "Morning, Jimmy" to the screen.

Now suppose this program needs to be globalized. Oh, no, I'm Chinese. I don't understand what "Morning" means. What should I do? Well, let's add a Chinese version of the greeting method:

public void ChineseGreeting(string name){
    Console.WriteLine("good morning, " + name);
}

At this time, GreetPeople also needs to be changed. Otherwise, how to judge which version of Greeting method is appropriate? Before this, we'd better define another enumeration as the basis for judgment:

public enum Language{
    English, Chinese
}
public void GreetPeople(string name, Language lang){
    //Do some extra things, such as initialization, which is omitted here
    swith(lang){
        case Language.English:
           EnglishGreeting(name);
           break;
       case Language.Chinese:
           ChineseGreeting(name);
           break;
    }
}

OK, although this solves the problem, it's easy to think that the scalability of this solution is very poor. If we need to add Korean and Japanese versions in the future, we have to repeatedly modify the enumeration and GreetPeople() methods to meet the new requirements.

Before considering a new solution, let's take a look at the method signature of GreetPeople:

public void GreetPeople(string name, Language lang)

Let's just look at string name. Here, string is the parameter type and name is the parameter variable. When we assign the name string "jimmy", it represents the value of "jimmy"; When we give it "Zhang Ziyang", it represents the value of "Zhang Ziyang". Then, we can perform other operations on this name in the method body. Hey, this is nonsense. I knew it just after learning the program.

If you think about it again, if the greenpeople () method can accept a parameter variable, this variable can represent another method. When we assign EnglishGreeting to this variable, it represents the EnglsihGreeting() method; When we assign Chinese greeting to it, it represents the Chinese greeting () method. If we name this parameter variable MakeGreeting, can we also assign a value to this MakeGreeting parameter (Chinese greeting or EnglsihGreeting, etc.) when calling the greenpeople () method, just as when assigning a value to name? Then, in the method body, we can also use MakeGreeting like other parameters. However, because MakeGreeting represents a method, its use method should be the same as the assigned method (such as Chinese greeting), such as:

MakeGreeting(name);

OK, now that we have an idea, let's change the greenpeople () method. Then it should look like this:

public void GreetPeople(string name, *** MakeGreeting){
    MakeGreeting(name);
}

Note that * * * usually places the type of parameter in this position, but so far, we just think that there should be a parameter representing the method, and rewrite the greenpeople method according to this idea. Now there is a big problem: what type should the MakeGreeting parameter representing the method be?

NOTE: enumeration is no longer needed here, because when assigning a value to MakeGreeting, it dynamically determines which method to use, Chinese greeting or English greeting. Within these two methods, a distinction has been made between "morning" and "good morning".

Smart, you should have thought that it's time for the delegate to come out. But before talking about the delegate, let's look at the signatures of the Chinese greeting () and EnglishGreeting() methods represented by the MakeGreeting parameter:

public void EnglishGreeting(string name)
public void ChineseGreeting(string name)

Just as name can accept "true" and "1" of String type, but cannot accept true of bool type and 1 of int type. The parameter type definition of MakeGreeting should be able to determine the type of method that MakeGreeting can represent. Further, it is the parameter type and return type of the method that MakeGreeting can represent.

Thus, the delegate appears: it defines the type of method that the MakeGreeting parameter can represent, that is, the type of the MakeGreeting parameter.

NOTE: if the above sentence is tongue twisty, I translate it into this: string defines the type of value that the name parameter can represent, that is, the type of the name parameter.

Definition of delegation in this example:

public delegate void GreetingDelegate(string name);

You can compare it with the signature of the EnglishGreeting() method above. Apart from adding the delegate keyword, are the rest exactly the same?

Now, let's change the GreetPeople() method again, as follows:

public void GreetPeople(string name, GreetingDelegate MakeGreeting){
    MakeGreeting(name);
}

As you can see, the position of Delegate greeningdelegate is the same as that of string. String is a type, so greeningdelegate should also be a type, or class. But the declaration of delegates is completely different from that of classes. What's the matter? In fact, delegates do compile into classes at compile time. Because Delegate is a class, delegates can be declared anywhere a class can be declared. More will be described below. Now, please take a look at the complete code of this example:

using System;
using System.Collections.Generic;
using System.Text;

namespace Delegate {
     //Defines a delegate that defines the type of method that can be represented
     public delegate void GreetingDelegate(string name);
        class Program {

           private static void EnglishGreeting(string name) {
               Console.WriteLine("Morning, " + name);
           }

           private static void ChineseGreeting(string name) {
               Console.WriteLine("good morning, " + name);
           }

           //Notice this method, which accepts a method of type greeningdelegate as an argument
           private static void GreetPeople(string name, GreetingDelegate MakeGreeting) {
               MakeGreeting(name);
            }

           static void Main(string[] args) {
               GreetPeople("Jimmy Zhang", EnglishGreeting);
               GreetPeople("Zhang Ziyang", ChineseGreeting);
               Console.ReadKey();
           }
        }
    }

The output is as follows:

Morning, Jimmy Zhang
 good morning, Zhang Ziyang

Now let's summarize the entrustment:

A delegate is a class that defines the type of a method so that a method can be passed as a parameter of another method. This method of dynamically assigning a method to a parameter can avoid the extensive use of if else (switch) statements in the program and make the program more extensible.

Bind method to delegate

See here, is it a little like waking up from a dream? So, are you thinking: in the above example, I don't have to directly assign a value to the name parameter in the greenpeople() method. I can use the variable like this:

static void Main(string[] args) {
    string name1, name2;
    name1 = "Jimmy Zhang";
    name2 = "Zhang Ziyang"; 

     GreetPeople(name1, EnglishGreeting);
     GreetPeople(name2, ChineseGreeting);
    Console.ReadKey();
}

Since the delegate GreetingDelegate has the same status as the type string and defines a parameter type, can I also use the delegate in this way?

static void Main(string[] args) {
    GreetingDelegate delegate1, delegate2;
    delegate1 = EnglishGreeting;
    delegate2 = ChineseGreeting;

    GreetPeople("Jimmy Zhang", delegate1);
        GreetPeople("Zhang Ziyang", delegate2);
        Console.ReadKey();
}

As you expected, there is no problem. The program outputs as expected. Here, I want to talk about a characteristic of delegation different from string: multiple methods can be assigned to the same delegate, or multiple methods can be bound to the same delegate. When calling this delegate, the bound methods will be called in turn. In this example, the syntax is as follows:

static void Main(string[] args) {
    GreetingDelegate delegate1;
    delegate1 = EnglishGreeting; // Assign a value to the variable of the delegate type first
    delegate1 += ChineseGreeting;   // Bind another method to this delegate variable

     // The EnglishGreeting and ChineseGreeting methods will be called successively.
    GreetPeople("Jimmy Zhang", delegate1);  
    Console.ReadKey();
}

Output is:

Morning, Jimmy Zhang
 good morning, Jimmy Zhang

In fact, we can bypass the greenpeople method and directly call EnglishGreeting and ChineseGreeting through delegation:

static void Main(string[] args) {
    GreetingDelegate delegate1;
    delegate1 = EnglishGreeting; // Assign a value to the variable of the delegate type first
    delegate1 += ChineseGreeting;   // Bind another method to this delegate variable

    // The EnglishGreeting and ChineseGreeting methods will be called successively.
    delegate1 ("Jimmy Zhang");   
    Console.ReadKey();
}

NOTE: there is no problem in this example, but look back at the definition of GreetPeople() above. In it, you can do some work that needs to be done for both EnglshihGreeting and Chinese greeting. I have omitted it for simplicity.

Note that "=" used for the first time is the syntax of assignment; The second time, the "+ =" is used, which is the syntax of binding. If "+ =" is used for the first time, a compilation error of "using unassigned local variables" will appear.

We can also use the following code to simplify this process:

GreetingDelegate delegate1 = new GreetingDelegate(EnglishGreeting);
delegate1 += ChineseGreeting;   // Bind another method to this delegate variable

Seeing this, you should note that the first statement of this code is very similar to instantiating a class. You can't help thinking that the compilation error of "+ =" can't be used when binding a delegate for the first time. Maybe you can use this method to avoid it:

GreetingDelegate delegate1 = new GreetingDelegate();
delegate1 += EnglishGreeting;   // This time, the binding syntax is "+ =".
delegate1 += ChineseGreeting;   // Bind another method to this delegate variable

In practice, however, a compilation error occurs: the "greeningdelegate" method does not take an overload of "0" parameters. Although this result makes us feel a little frustrated, the compilation prompt: "overload without 0 parameters" reminds us of the class constructor again. I know you can't help but want to find out, but before that, we need to introduce the basic knowledge and application.

Since a method can be bound to a delegate, there should also be a way to unbind the method. It is easy to think that the syntax is "-":

static void Main(string[] args) {
    GreetingDelegate delegate1 = new GreetingDelegate(EnglishGreeting);
    delegate1 += ChineseGreeting;   // Bind another method to this delegate variable

    // The EnglishGreeting and ChineseGreeting methods will be called successively.
    GreetPeople("Jimmy Zhang", delegate1);  
    Console.WriteLine();

    delegate1 -= EnglishGreeting; //Unbind EnglishGreeting method
    // Only Chinese greeting will be called  
    GreetPeople("Zhang Ziyang", delegate1); 
    Console.ReadKey();
}

Output is:

Morning, Jimmy Zhang
 good morning, Jimmy Zhang
 good morning, Zhang Ziyang

Let's summarize the entrustment again:

Using delegates, you can bind multiple methods to the same delegate variable. When calling this variable (the word "call" is used here because this variable represents a method), you can call all bound methods in turn.

The origin of the event

Let's continue to think about the above program: the above three methods are defined in the Programe class for the convenience of understanding. In practical application, greenpeople is usually in one class, and Chinese greeting and EnglishGreeting are in another class. Now that you have a preliminary understanding of delegation, it's time to improve the above example. Suppose we put GreetingPeople() in a class called GreetingManager, then the new program should look like this:

namespace Delegate {
    //Defines a delegate that defines the type of method that can be represented
    public delegate void GreetingDelegate(string name);
    
    //New greeningmanager class
    public class GreetingManager{
       public void GreetPeople(string name, GreetingDelegate MakeGreeting) {
           MakeGreeting(name);
       }
    }

    class Program {
       private static void EnglishGreeting(string name) {
           Console.WriteLine("Morning, " + name);
       }

       private static void ChineseGreeting(string name) {
           Console.WriteLine("good morning, " + name);
       }

       static void Main(string[] args) {
           // ... ...
        }
    }
}

At this time, if you want to achieve the output effect demonstrated earlier, I think the Main method should be as follows:

static void Main(string[] args) {
    GreetingManager gm = new  GreetingManager();
    gm.GreetPeople("Jimmy Zhang", EnglishGreeting);
    gm.GreetPeople("Zhang Ziyang", ChineseGreeting);
}

We run this code, well, there's no problem. The program outputs as expected:

Morning, Jimmy Zhang

good morning, Zhang Ziyang

Now, suppose we need to use the knowledge learned in the previous section to bind multiple methods to the same delegate variable. What should we do? Let's rewrite the code again:

static void Main(string[] args) {
    GreetingManager gm = new  GreetingManager();
    GreetingDelegate delegate1;
    delegate1 = EnglishGreeting;
    delegate1 += ChineseGreeting;

    gm.GreetPeople("Jimmy Zhang", delegate1);
}
Output:
Morning, Jimmy Zhang
 good morning, Jimmy Zhang

Here, we can't help thinking: object-oriented design pays attention to the encapsulation of objects. Since we can declare a variable of delegate type (delegate1 in the above example), why don't we encapsulate this variable into the GreetManager class? Isn't it more convenient to use it in the client of this class? So we rewrite the GreetManager class, like this:

public class GreetingManager{
    //Declare the delegate1 variable inside the GreetingManager class
    public GreetingDelegate delegate1;  

    public void GreetPeople(string name, GreetingDelegate MakeGreeting) {
       MakeGreeting(name);
    }
}

Now we can use this delegate variable as follows:

static void Main(string[] args) {
    GreetingManager gm = new  GreetingManager();
    gm.delegate1 = EnglishGreeting;
    gm.delegate1 += ChineseGreeting;

    gm.GreetPeople("Jimmy Zhang", gm.delegate1);
}



Output is:

Morning, Jimmy Zhang
 good morning, Jimmy Zhang

Although there is no problem doing so, we find this statement strange. When calling gm.GreetPeople method, the delegate1 field of GM is passed again:

gm.GreetPeople("Jimmy Zhang", gm.delegate1);

In that case, why don't we modify the GreetingManager class as follows:

public class GreetingManager{
    //Declare the delegate1 variable inside the GreetingManager class
    public GreetingDelegate delegate1;  

    public void GreetPeople(string name) {
        if(delegate1!=null){     //If there is a way to register delegate variables
          delegate1(name);      //Calling methods through delegates
       }
    }
}

On the client side, the call looks more concise:

static void Main(string[] args) {
    GreetingManager gm = new  GreetingManager();
    gm.delegate1 = EnglishGreeting;
    gm.delegate1 += ChineseGreeting;

    gm.GreetPeople("Jimmy Zhang");      //Note that you do not need to pass the delegate1 variable this time
}


Output is:

Morning, Jimmy Zhang
 good morning, Jimmy Zhang

Although this has achieved the desired effect, there are still problems:

Here, delegate1 is no different from the string variables we usually use. As we know, not all fields should be declared public. The appropriate approach is to be public when it should be public and private when it should be private.

Let's first see what happens if delegate1 is declared private? The result: it's just funny. Because the purpose of declaring a delegate is to expose it to the client of the class for method registration, you declare it private, and the client is not visible to it at all. What's the use of it?

Let's see what happens when delegate1 is declared public? The result is that it can be randomly assigned at the client, which seriously destroys the encapsulation of the object.

Finally, the first method is registered with "=", which is the assignment syntax, because instantiation is required, and the second method is registered with "+ =". However, whether it is assignment or registration, the method is bound to the delegate. There is no difference except that the calling order is different. Isn't this very awkward?

Now let's think, what would you do if delegate1 was not a delegate type, but a string type? The answer is to use attributes to encapsulate fields.

So, Event comes out. It encapsulates the variables of the delegate type, so that: inside the class, whether you declare it public or protected, it is always private. Outside the class, the access qualifiers for registering "+ =" and unregistering "-" = "are the same as those you use when declaring events.

We rewrite the GreetingManager class, which looks like this:

public class GreetingManager{
    //This time we are here to declare an event
    public event GreetingDelegate MakeGreet;

    public void GreetPeople(string name) {
        MakeGreet(name);
    }
}

It is easy to notice that the only difference between the declaration of the makegreen event and the previous declaration of the delegate variable delegate1 is the addition of an event keyword. Here, in combination with the above explanation, you should understand that there is nothing difficult to understand about events. Declaring an event is just like declaring a variable of encapsulated delegate type.

To prove the above inference, if we rewrite the Main method as follows:

static void Main(string[] args) {
    GreetingManager gm = new  GreetingManager();
    gm.MakeGreet = EnglishGreeting;         // Compilation error 1
    gm.MakeGreet += ChineseGreeting;

    gm.GreetPeople("Jimmy Zhang");
}

You get a compilation error: the event "delegate. Greengmanager. Makegreen" can only appear to the left of + = or - = (except when used from type "delegate. Greengmanager").

Compiled code for events and delegates

At this time, we comment out the lines with compilation errors, then recompile them, and then explore the declaration statement of event with the help of the replacer to see why such errors occur:

public event GreetingDelegate MakeGreet;

It can be seen that although we declare makegreen as public in the GreetingManager, in fact, makegreen will be compiled into private fields. No wonder the above compilation error occurs, because it is not allowed to access by assignment outside the GreetingManager class, which verifies our inference above.

Let's take a closer look at the code generated by makegreen:

private GreetingDelegate MakeGreet; //A declaration of an event is actually a declaration of a private delegate variable
 
[MethodImpl(MethodImplOptions.Synchronized)]
public void add_MakeGreet(GreetingDelegate value){
    this.MakeGreet = (GreetingDelegate) Delegate.Combine(this.MakeGreet, value);
}

[MethodImpl(MethodImplOptions.Synchronized)]
public void remove_MakeGreet(GreetingDelegate value){
    this.MakeGreet = (GreetingDelegate) Delegate.Remove(this.MakeGreet, value);
}

It is now clear that the makegreen event is indeed a greeningdelegate type delegate, but whether it is declared public or not, it is always declared private. In addition, it has two methods: add_ Makegreen and remove_ Makegreen, the two methods are used to register the delegate type method and unregister the delegate type method respectively. In fact, it means: "+ =" corresponds to add_ Makegreen, "- =" corresponds to remove_MakeGreet. The access restrictions of these two methods depend on the access restrictions when declaring the event.

In add_ Inside the makegreen () method, it actually calls the Combine() static method of System.Delegate, which is used to add the current variable to the delegate linked list. We mentioned twice earlier that a delegate is actually a class. When we define a delegate:

public delegate void GreetingDelegate(string name);

When the compiler encounters this code, it will generate the following complete class:

public sealed class GreetingDelegate:System.MulticastDelegate{
    public GreetingDelegate(object @object, IntPtr method);
    public virtual IAsyncResult BeginInvoke(string name, AsyncCallback callback, object @object);
    public virtual void EndInvoke(IAsyncResult result);
    public virtual void Invoke(string name);
}

For more details about this class, please refer to CLR Via C#, and other related books, which will not be discussed here.

Posted by duke on Mon, 20 Sep 2021 12:27:01 -0700