[C# Study Notes] Delegation and events

Insert image description here


entrust

Delegates provide a late binding mechanism in .NET. Late binding means that the caller provides at least one method in the algorithm you create to implement part of the algorithm.

To put it more simply, we can process a large number of methods by calling delegates. A delegate is like a container for functions. In my previous article, I compared it to a small order written by a waiter when ordering food. When the delegate is triggered, the order is given to the chef, and the chef will cook it in order. Bring out our order.


Definition of delegation

When we define a delegate, we need to use delegatekeywords.

    delegate void MyFun();// 委托不可重载
    delegate void MyFun<T>();// 带泛型的函数名MyFun<T>和MyFun不同
    delegate int MyFun2(int i);

Delegates are not overloadable. Regardless of modifying the function return value definition or adding parameters, as long as a delegate with the same function name exists, it will not pass compilation. (But the function names with generics are not the same, for example, MyFun, MyFun, MyFun<T,K> can exist at the same time)

Delegate instantiation

When you want to use a delegate, you need to instantiate it first. There are two ways to instantiate it. New an object and assign the first function to it:

    int Input(int i)
    {
    
    
        Debug.Log(i);
        return i;
    }
    
void Start()
{
    
    
	MyFun2 myFun = new MyFun2(Input);// new的时候需要定义第一个调用函数
	MyFun myFun1 = null;// 委托初始化可为空,但是空委托触发时会报错
}

It should be noted that: first, the initialization of the delegate must be performed in the method and cannot be initialized during definition; second, the format of the delegate and the format of the function must be exactly the same. Whatever type is specified for the return value of the delegate, the function assigned to the delegate must also be of the same type, and as many input parameters as the delegate has, the function must also have the same input parameters:

delegate int MyFun2(int i);
int Input(int i)
{
    
    
  return i;
}
void Input1(int i)
{
    
    
}
int Input2(string i)
{
    
    
  return 1;
}
MyFun2 myFun = new MyFun2(Input);
myFun += Input1; // 返回值不同,报错
myFun = new MyFun2(Input2); // 入参不同,报错

Delegated call

When calling the delegate, we need to ensure that the delegate is not empty. We can use reflection and direct call methods to call the delegate:

myFun1();//委托为空执行会报错
myFun(100);
myFun.Invoke(100); //带有入参的委托需要在调用时给出入参
myFun1.Invoke(); //委托为空执行会报错
myFun1?.Invoke(); //使用?.Invoke(),当委托为空时不调用

multicast delegation

A delegate can contain multiple methods and be triggered. Such a delegate is called a multicast delegate. For the methods in the delegate, you can simply use the following statements to increase or decrease:

myFun1 += Fun;
myFun1 -= Fun2;// 当减去方法的时候,若委托中没有对应函数,
//编译(即使委托为空)和执行都不会报错
myFun1 += SayHi;

In a multicast delegate, the triggering order of events in the delegate is executed in the order of the methods we add to the delegate when executing the statement. If method A is added first and then method B is added, A will be executed first and then B.


Why use delegation?

Please see an example below:

    class Test
    {
    
    
        public MyFun fun;
        public MyFun2 fun2;
        int i=10;
        public void TestFun(MyFun fun,MyFun2 fun2)
        {
    
    
			i= i*100;
			fun2.Invoke(i);
			fun.Invoke();
        }
    }

In the above class, we use a function to receive two delegates, then process the parameters in the function iand define the calling order of the two delegates. Ultimately, this class can dispatch this function method when needed after instantiation.

MonsterWhen we need to process a series of methods after the class is instantiated, for example, if a class is instantiated 小怪A, icorresponding to its damage, fun1 and fun2 correspond to a series of reactions that will be triggered after the attack. In this way, the method of attacking a mob is simply completed.

On the one hand, if we want to call a function, first of all, the function cannot be passed as a parameter to other functions. Secondly, if the scheduling function will consider a lot of issues: for example, whether the access modification of the function that needs to be scheduled is public, whether it must be first Introduce other classes, what to do if you want to modify this attack method, etc. When using a delegate, you don't need to consider these issues. You only need to ensure that the format of the delegate and the calling method are consistent. Throw the delegate into the method and call it directly. The whole problem is abstracted to only consider when we should trigger the delegate.
On the other hand, in our structure, the delegate is triggered instead of the function, which means that we can modify the function triggered by the attack method at will, simply adding or subtracting methods to the delegate.

Another example of the use of delegates that I want to introduce comes from the official Unity system. In the officially provided New InputSystem, each keystroke triggers a delegate. This allows us to modify the methods corresponding to the keys as easily as the interface. In the past, we used to ifdetect keystrokes and then schedule methods. Now we can directly add the scheduling method to delegated multicast through delegation. This makes it easier to modify and more flexible.


official commission

Although we can define delegates by ourselves, after all, the code is for people to read. Someone may accept your code and not know whether the corresponding type is a class, a structure, or something else. Only when he looks at the reference will he know that it is a delegate. . The official has thoughtfully provided some named commissions:

Action action = test.Fun; // void Action() 无参无返回委托
Action<string> action1 = NewString; // void Action<T>() 有参无返回泛型委托,最多接受16个泛型传入
action1 += Tstring; // 如果函数同样接收泛型,那么函数的泛型会自动接受Action委托声明时给出的对应泛型
Func<int> func = Input;// T Func<out T>(); 无参带返回值泛型委托,使用out修饰代表该委托是协变的,最后一个泛型决定返回值类型
Func<int,string,int> func1 = Input;// Result Func<T1,T2...Result>(T1 arg1, T2 arg2...) 有参带返回值泛型委托,最多接受16个泛型传入

There are two official delegations in total: Actiondelegation without a return value, Funcand delegation with a return value. These two delegates have generic definitions with the same name, and can accept up to 16 generics, each of which corresponds to an input parameter.

Use them just like normal delegates, for example:

void Input(){
    
    }
int Input()
{
    
    
   return 1;
}
int Input<T1,T2>(T1 a,T2 b)
{
    
    
   return 1;
}
void Tstring<T>(T i){
    
    }
void NewString(string i){
    
    }

Action action = Input; // Action无入参无返回值,对应delegate void Action()
Action<string> action1 = NewString;// Action有入参无返回值,对应void Action<T>(T t)
// 上句对应的NewString类型也要完全一致,也是无返回值,入参一个,类型为string
Func func = Input;// Func无入参有返回值,对应TResult Func<out TResult>(),out修饰协变
// 注意当使用Func委托的时候,至少需要定义一个泛型,这个泛型对应的不是入参而是返回值的类型
Func<int,string,int> func1 = Input;// 同理,右侧最后一个泛型int代表了返回值的类型
// Func定义的三个泛型,则需要委托的方法要有返回值,且有两个入参

The above is the usage method of the official delegate. It is used when a return value is not needed Aciton. It is used when a return value is needed Func. The last generic type also needs to be defined to represent the type of the return value.

Generic methods and generic delegates

int Input<T1,T2>(T1 a,T2 b)
{
    
    
   return 1;
}
Func<int,string,int> func1 = Input;

In the above code, the function Inputdefines two generics, and Functhere are three generics in the delegate. In fact, they match. After all, Functhe last generic represents the type of the return value.

There are now the following definitions:

T Input<T1,T2,T>(T1 a,T2 b)
{
    
    
   T t = default(T);
   return t;
}
Func<int,string,int> func1 = Input; // 报错

The above code looks very reasonable. The three generic types are assigned to the three generic types of Input in sequence, but in fact it does not work. FuncOnly the first two generics will be defined to the function, so the third generic cannot be inferred by the compiler.

Generic delegates also accept tuples, by default the first item of the tuple is when called .Item1, the second item is .Item2, and so on.

Func<(int, int, int), (int, int, int)> doubleThem = ns => (2 * ns.Item1, 2 * ns.Item2, 2 * ns.Item3);
var numbers = (2, 3, 4);
var doubledNumbers = doubleThem(numbers);

Of course, you can also customize the name of each item in the tuple like this:

Func<(int n1, int n2, int n3), (int, int, int)> doubleThem = ns => (2 * ns.n1, 2 * ns.n2, 2 * ns.n3);

event

Once you learn delegation, you also learn events. Events and delegation are basically the same:

class Test
{
    
    
        delegate void NewDel();
        event NewDel MyFun; // 注意,定义事件时其访问性必须与委托一致
}

When defining an event, use eventkeywords to modify the delegate name and name the definition of the event delegate. The use of events and delegates is basically the same, so I won’t go into details. The only difference is that events cannot be assigned or called outside the class:

    class Test
    {
    
    
        public delegate void NewDel();
        public NewDel del = null;
        public event NewDel MyFun;
        public Test()
        {
    
    
            del = NewFun;
            MyFun = NewFun;
        }
        public void NewFun()
        {
    
    

        }
    }
    void Start()
    {
    
    
        Test t = new Test();
        t.del();
        t.del.Invoke();
        t.MyFun(); // 报错
        t.MyFun.Invoke(); // 报错
        t.MyFun = null; // 报错
        t.MyFun += Appli;
        t.MyFun -= Appli;
    }

    void Appli()
    {
    
    

    }

Outside the class, direct operations are not possible Event, only addition and subtraction functions can be performed. And events cannot be used as temporary variables in functions. Intelligence exists as members in classes, interfaces, and structures.

Why have events?

  1. Prevent external empty commissions from being placed at will
  2. Prevent external arbitrary calls to delegates
  3. Events are equivalent to encapsulating the delegate, making it safer

The difference between events and delegates:

  • Events cannot be assigned values ​​externally, and can only be added or subtracted externally through function delegation.
  • Events cannot be executed externally, but delegation can be executed anywhere
  • Events cannot be used as temporary variables in functions, but delegates can

Digression - Delegate and Observer Patterns

First, let’s briefly introduce what the observer pattern is. The observer pattern is a one-to-many dependency relationship between objects. When the state of an object changes, all objects that depend on it are notified and automatically updated.

For example, there are n foreign trade companies, and these companies all have import and export businesses. When the RMB depreciates, the depreciation of the RMB will cause these companies to be more inclined to export business, while the appreciation of the RMB will be more inclined to the import business:

    class Company
    {
    
    
        public void Update(bool 贬值了)
        {
    
    
            if (贬值了) {
    
     Debug.Log("出口"); }
            else {
    
     Debug.Log("进口"); }
        }
    }
    class CHY
    {
    
    
        private List<Company> Companies = new List<Company>();
        bool state = false;
        void 贬值()// 原谅我不懂贬值的英文,幸好C#可以起中文名,作为举例足够了
        {
    
    
            state = true;
            Debug.Log("人民币贬值");
        }
        void 升值()
        {
    
    
            state = false;
            Debug.Log("人民币升值");
        }
        bool setState()
        {
    
    
            // 判断升值还是贬值的代码
            return state;
        }
        void getState()
        {
    
    
            foreach(var company in Companies)
            {
    
    
                company.Update(state);
            }
        }
    }

In the above code, if the RMB appreciates or depreciates, getState()those observers (companies) will be notified through it. When the observers find changes in the RMB exchange rate, they will choose the corresponding strategic plan. This is a simple observer pattern.

In the observer pattern, it is obvious that these related classes have strong dependencies. If the observed object changes, notifications will be broadcast to all observers, and the observers will respond accordingly.

So what is the relationship between delegation and observer pattern? If you think about it carefully, in fact, delegates and observers are essentially similar. Their processing modes are:
start-notify-process one by one.

Now let us add the above observer event to a delegate, that is:

Func<bool> ChangeState = setState;// 代码有点小问题,意思到了就行
ChangeState += Company1.Update;
ChangeState += Company2.Update;
......
ChangeState.Invoke(state);

When we triggered ChangeStatethe event, the RMB status changed, and the company attached to the commission was also notified. The functions implemented are the same as those of the observer pattern. So why do we use delegation to implement it? Becausedecoupling

In the first example, the relationship between the observer and the observed is so close that there is dependence and the observed needs to notify the observer. Coupling is too high. After using the delegate, there is no need to define the method in the observer getState(), and there is no need to define it List<Company>. You only need to add the observer's update status method to the delegate. The coupling between the two classes is greatly reduced. Using delegation, the observed is completely unaware of the existence of the observer. This is the true observer pattern.

Guess you like

Origin blog.csdn.net/milu_ELK/article/details/132410268