C #: simplified syntax of delegation, talk about anonymous methods and closures (Part 1)

Time:2022-5-2

0x00 Preface

Through previous blogDetailed description c#: the cook talks about the entrustment, which are hidden by the compiler and given by U3DWe implemented the process of using delegates to build our own messaging system. However, in daily development, there are still many developers who choose to alienate the entrustment for one reason or another, and one of the most common reasons is the resistance to the entrustment due to the strange syntax of the entrustment.

Therefore, the main goal of this paper is to introduce some simplified syntax of delegation, so as to reduce the resistance to delegation for developers with this mentality.

0x01 no need to construct delegate object

A common way to use delegation is like the following line of code:

this.unit.OnSubHp += new BaseUnit.SubHpHandler(this.OnSubHp);

Onsubhp in brackets is a method, which is defined as follows:

private void OnSubHp (BaseUnit source, float subHp, DamageType damageType, HpShowType showType)
    {
        string unitName = string.Empty;
        String missstr = "dodge";
        string damageTypeStr = string.Empty;
        string damageHp = string.Empty;
        
        if(showType == HpShowType.Miss)
        {
            Debug.Log(missStr);
            return;
        }
    
        if(source.IsHero)
        {
            Unitname = "hero";
        }
        else
        {
            Unitname = "soldier";
        }
        damageTypeStr = damageType == DamageType. Critical ?  "Critical hit": "ordinary attack";
        damageHp = subHp.ToString();
        Debug.Log(unitName + damageTypeStr + damageHp);
    }

The first line of code listed above means to this The onsubhp event of unit registers the address of the onsubhp method. When the onsubhp event is triggered, it notifies to call the onsubhp method. The meaning of this line of code is to obtain a wrapper that wraps the callback method onsubhp by constructing an instance of the subhphandler delegate type, so as to ensure that the callback method can only be called in a type safe manner. At the same time, through this wrapper, we also get support for the delegation chain. However, more programmers obviously prefer a simple expression. They don’t need to really understand the meaning of creating a delegate instance to obtain the wrapper, but just need to register the corresponding callback method for the event. For example, the following line of code:

this.unit.OnSubHp += this.OnSubHp;

The reason why I can write like this has been explained in my previous blog. Although the “+ =” operator expects an object of subhphandler delegate type, this Onsubhp method should be wrapped by subhphandler delegate type object. However, because c#’s compiler can infer by itself, the code for constructing subhphandler delegate instance can be omitted, making the code more readable to programmers. However, the compiler has not changed much behind the scenes. Although the developer’s syntax has been simplified, the compiler will still create a new instance of subhphandler delegate type when generating CIL code.

In short, C # allows you to omit the code that constructs an instance of a delegate type by specifying the name of the callback method.

0x02 anonymous method

In the previous blog post, we can see that when using delegates, we often declare corresponding methods. For example, parameters and return types must conform to the method prototype determined by the delegate type. Moreover, in the actual game development process, we often need this mechanism of delegation to deal with very simple logic, but correspondingly, we must create a new method to match the delegate type, which seems to make the code very bloated. Therefore, in the version of c#2, the mechanism of anonymous method is introduced. What is anonymous method? Let’s take a look at a small example.

using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System;     

public class DelegateTest : MonoBehaviour {
    
       // Use this for initialization
       void Start () {
              //Use anonymous methods for action < T > delegate types
              Action<string> tellMeYourName = delegate(string name) {
                     string intro = "My name is ";
                     Debug.Log(intro + name);
              };
    
              Action<int> tellMeYourAge = delegate(int age) {
                     string intro = "My age is ";
                     Debug.Log(intro + age.ToString());
              };
              tellMeYourName("chenjiadong");
              tellMeYourAge(26);
       }
    
       // Update is called once per frame
       void Update () {
    
       }
}

Mount the delegatetest script on an object in a game scene and run the editor. You can see the following output in the debugging window.

My name is chenjiadong

UnityEngine.Debug:Log(Object)

My age is 26

UnityEngine.Debug:Log(Object)

Before explaining this code, I need to introduce two common generic delegate types: action < T > and func < T >. Their main manifestations are as follows:

public delegate void Action();
public delegate void Action<T1>(T1 arg1);
public delegate void Action<T1, T2>(T1 arg1, T2 arg2);
public delegate void Action<T1, T2, T3>(T1 arg1, T2 arg2, T3 arg3);
public delegate void Action<T1, T2, T3, T4>(T1 arg1, T2 arg2, T3 arg3, T4 arg4);
public delegate void Action<T1, T2, T3, T4, T5>(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5);

You can see from the definition form of action < T >. Action < T > has no return value. Applies to any method that does not return a value.

public delegate TResult Func<TResult>();
public delegate TResult Func<T1, TResult>(T1 arg1);
public delegate TResult Func<T1, T2, TResult>(T1 arg1, T2 arg2);
public delegate TResult Func<T1, T2, T3, TResult>(T1 arg1, T2 arg2, T3 arg3);
public delegate TResult Func<T1, T2, T3, T4, TResult>(T1 arg1, T2 arg2, T3 arg3, T4 arg4);
public delegate TResult Func<T1, T2, T3, T4, T5, TResult>(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5);

Func < T > delegation is defined relative to action < T >. Action < T > is a method delegate without return value, and func < T > is a delegate with return value. The type of the return value is constrained by the type defined in the generic type.

Well, after readers have a preliminary understanding of the two common generic delegate types of C #, let’s take a look at the code above that uses anonymous methods. First, we can see the syntax of anonymous methods: first use the delegate keyword, then the parameter part if there are parameters, and finally a code block to define the operation on the delegate instance. Through this code, we can also see that things can be done in the general method body, and anonymous functions can do the same. For the implementation of anonymous methods, we should also thank the compiler for hiding a lot of complexity behind the scenes, because in CIL code, the compiler creates a corresponding method for each anonymous method in the source code, and adopts the same operation as when creating the delegate instance. The created method is wrapped by the delegate instance as a callback function. It is precisely because the compiler creates methods corresponding to anonymous methods for us, so the method names of these methods are automatically generated by the compiler. In order not to conflict with the method names declared by the developer, the readability of the method names generated by the compiler is very poor.

Of course, if the above code still seems bloated at first glance, can it be used directly without assigning an instance of a delegate type? The answer is yes, which is also one of the most commonly used anonymous methods. That is to use the anonymous method as a parameter of another method, because this can reflect the value of the anonymous method – simplifying the code. Let’s take a look at a small example. Remember the list < T > list? < T > get the action defined in the < T > action list as a parameter of each method. The following code will demonstrate this. We use anonymous methods to get normalized for the elements in the list (vector vector3).

using UnityEngine;
using System.Collections;
using System.Collections.Generic;
    
public class ActionTest : MonoBehaviour {
    
       // Use this for initialization
       void Start () {
              List<Vector3> vList = new List<Vector3>();
              vList.Add(new Vector3(3f, 1f, 6f));
              vList.Add(new Vector3(4f, 1f, 6f));
              vList.Add(new Vector3(5f, 1f, 6f));
              vList.Add(new Vector3(6f, 1f, 6f));
              vList.Add(new Vector3(7f, 1f, 6f));
    
              vList.ForEach(delegate(Vector3 obj) {
                     Debug.Log(obj.normalized.ToString());
              });
       }          

       // Update is called once per frame
       void Update () {
    
       }
}

We can see that an anonymous method with parameter vector3:


delegate(Vector3 obj) {
       Debug.Log(obj.normalized.ToString());
}

In fact, it is passed into the foreach method of list as a parameter. After this code is executed, we can observe the output results in the debugging window of unity3d. The contents are as follows:

(0.4, 0.1, 0.9)

UnityEngine.Debug:Log(Object)

(0.5, 0.1, 0.8)

UnityEngine.Debug:Log(Object)

(0.6, 0.1, 0.8)

UnityEngine.Debug:Log(Object)

(0.7, 0.1, 0.7)

UnityEngine.Debug:Log(Object)

(0.8, 0.1, 0.6)

UnityEngine.Debug:Log(Object)

So, can the expression of anonymous methods be more concise? Of course, if readability is not considered, we can also write anonymous methods in this form:

vList.ForEach(delegate(Vector3 obj) {Debug.Log(obj.normalized.ToString());});

Of course, this is just a reference for readers. In fact, this form with poor readability is not recommended.

In addition to the action < T > delegate type whose return type is void, the above also mentioned another delegate type, func < T >. Therefore, the above code can be modified to the following form, so that anonymous methods can have return values.

using UnityEngine;
using System;
using System.Collections;
using System.Collections.Generic;
    
public class DelegateTest : MonoBehaviour {
    
       // Use this for initialization
       void Start () {
              Func<string, string> tellMeYourName = delegate(string name) {
                     string intro = "My name is ";
                     return intro + name;
              };
    
              Func<int, int, int> tellMeYourAge = delegate(int currentYear, int birthYear) {
                     return currentYear - birthYear;
              };
    
              Debug.Log(tellMeYourName("chenjiadong"));
              Debug.Log(tellMeYourAge(2015, 1989));
       }
    
       // Update is called once per frame
       void Update () {

       }
}

The < return > type is assigned to the anonymous method of func > and the return value is assigned to the anonymous method of func >. Run the above c# script. In the debugging window of unity3d, we can see the following output:

My name is chenjiadong

UnityEngine.Debug:Log(Object)

26

UnityEngine.Debug:Log(Object)

We can see that we call the anonymous methods we defined through two delegate instances, tellmeyourname and tellmeyourage.

Of course, in c# language, in addition to the action < T > and func < T > mentioned just now, there are some preset delegate types that we may encounter in actual development, such as the delegate type predicate < T > whose return value is bool. Its signature is as follows:

public delegate bool Predicate<T> (T Obj);

The predicate < T > delegate type often plays a role in filtering and matching targets. Let’s take another look at a small example.

using UnityEngine;
using System;
using System.Collections;
using System.Collections.Generic;
    
public class DelegateTest : MonoBehaviour {
       private int heroCount;
       private int soldierCount;

       // Use this for initialization
       void Start () {
              List<BaseUnit> bList = new List<BaseUnit>();
              bList.Add(new Soldier());
              bList.Add(new Hero());
              bList.Add(new Soldier());
              bList.Add(new Soldier());
              bList.Add(new Soldier());
              bList.Add(new Soldier());
              bList.Add(new Hero());

              Predicate<BaseUnit> isHero = delegate(BaseUnit obj) {
                     return obj.IsHero;
              };

              foreach(BaseUnit unit in bList)
              {
                     if(isHero(unit))
                            CountHeroNum();
                     else
                            CountSoldierNum();
              }
              Debug. Log ("the number of heroes is:" + this. Herocount ");
              Debug. Log ("number of soldiers:" + this. Soldiercount);
       }

       private void CountHeroNum()
       {
              this.heroCount++;
       }     

       private void CountSoldierNum()
       {
              this.soldierCount++;
       }

       // Update is called once per frame
       void Update () {

       }
}

The above code judges whether the baseunit is a soldier or a hero by using the predict delegate type, and then counts the number of soldiers and heroes in the list. As we just said, predicate is mainly used for matching and filtering. After the above code is run, the following contents will be output:

Number of Heroes: 2

UnityEngine.Debug:Log(Object)

Number of soldiers: 5

UnityEngine.Debug:Log(Object)

Of course, in addition to filtering and matching targets, we often encounter the situation of sorting the list according to a certain condition. For example, to sort according to the hero’s maximum HP or according to the hero’s combat effectiveness, it can be said that sorting according to requirements is one of the most common requirements in the process of game system development. Then, can we also easily implement the sorting function through delegation and anonymous methods? C # has preset some convenient “tools” for us? The answer is still yes. We can easily sort the list through the comparison < T > delegate type provided by c# and anonymous method.

The signature of comparison < T > is as follows:

public delegate int Comparison(in T)(T x, T y)

Since the comparison < T > delegate type is the delegate version of icomparison < T > interface, we can further analyze its two parameters and return value. The following table:

C #: simplified syntax of delegation, talk about anonymous methods and closures (Part 1)

Well, now we have clarified the meaning of the parameters and return values of the comparison < T > delegate type. Then let’s define an anonymous method to use it to sort the hero list according to the specified criteria.

First, we redefine the hero class to provide attribute data of heroes.

using UnityEngine;
using System.Collections;

public class Hero : BaseUnit{
       public int id;
       public float currentHp;
       public float maxHp;
       public float attack;
       public float defence;

       public Hero()
       {
       }

       public Hero(int id, float maxHp, float attack, float defence)
       {
              this.id = id;
              this.maxHp = maxHp;
              this.currentHp = this.maxHp;
              this.attack = attack;
              this.defence = defence;
       }

       public float PowerRank
       {
              get
              {
                     return 0.5f * maxHp + 0.2f * attack + 0.3f * defence;
              }
       }

       public override bool IsHero
       {
              get
              {
                     return true;
              }
       }
}

Then use the comparison < T > delegate type and anonymous method to sort the hero list.

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

public class DelegateTest : MonoBehaviour {
       private int heroCount;
       private int soldierCount;
    
       // Use this for initialization
       void Start () {
              List<Hero> bList = new List<Hero>();
              bList.Add(new Hero(1, 1000f, 50f, 100f));
              bList.Add(new Hero(2, 1200f, 20f, 123f));
              bList.Add(new Hero(5, 800f, 100f, 125f));
              bList.Add(new Hero(3, 600f, 54f, 120f));
              bList.Add(new Hero(4, 2000f, 5f, 110f));
              bList.Add(new Hero(6, 3000f, 65f, 105f));

              //Sort by hero ID
              this.SortHeros(bList, delegate(Hero Obj, Hero Obj2){
                     return Obj.id.CompareTo(Obj2.id);
              }"," sort by hero ID ");

              //Sort by maxhp of Heroes
              this.SortHeros(bList, delegate(Hero Obj, Hero Obj2){
                     return Obj.maxHp.CompareTo(Obj2.maxHp);
              }"," sort by maxhp of heroes ");

              //Sort by hero's attack
              this.SortHeros(bList, delegate(Hero Obj, Hero Obj2){
                     return Obj.attack.CompareTo(Obj2.attack);
              }"," sort by hero's attack ");

              //Sort by hero's defense
              this.SortHeros(bList, delegate(Hero Obj, Hero Obj2){
                     return Obj.defence.CompareTo(Obj2.defence);
              }"," sort by hero's defense ");

              //Sort by hero's powerrank
              this.SortHeros(bList, delegate(Hero Obj, Hero Obj2){
                     return Obj.PowerRank.CompareTo(Obj2.PowerRank);
              }"," sort by hero's powerrank ");

       }

       public void SortHeros(List<Hero> targets ,Comparison<Hero> sortOrder, string orderTitle)
       {
//           targets.Sort(sortOrder);
              Hero[] bUnits = targets.ToArray();
              Array.Sort(bUnits, sortOrder);
              Debug.Log(orderTitle);
              foreach(Hero unit in bUnits)
              {
                     Debug.Log("id:" + unit.id);
                     Debug.Log("maxHp:" + unit.maxHp);
                     Debug.Log("attack:" + unit.attack);
                     Debug.Log("defense:" + unit.defence);
                     Debug.Log("powerRank:" + unit.PowerRank);
              }
       }

       // Update is called once per frame
       void Update () {

       }
}

In this way, we can easily realize the requirements of sorting by hero ID, maxhp, attack, defense and powerrank through anonymous functions, without writing an independent method for each sorting.

Unfinished to be continued