细说C#:委托的简化语法,聊聊匿名方法和闭包(上)

chenjd

0x00 前言

通过之前博客《匹夫细说C#:庖丁解牛聊委托,那些编译器藏的和U3D给的》的内容,我们实现了使用委托来构建我们自己的消息系统的过程。但是在日常的开发中,仍然有很多开发者因为这样或那样的原因而选择疏远委托,而其中最常见的一个原因便是因为委托的语法奇怪而对委托产生抗拒感。

因而本文的主要目标便是介绍一些委托的简化语法,为有这种心态的开发者们减轻对委托的抗拒心理。

0x01 不必构造委托对象

委托的一种常见的使用方式,就像下面的这行代码一样:

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

其中括号中的OnSubHp是方法,该方法的定义如下:

private void OnSubHp (BaseUnit source, float subHp, DamageType damageType, HpShowType showType)
    {
        string unitName = string.Empty;
        string missStr = "闪避";
        string damageTypeStr = string.Empty;
        string damageHp = string.Empty;
        
        if(showType == HpShowType.Miss)
        {
            Debug.Log(missStr);
            return;
        }
    
        if(source.IsHero)
        {
            unitName = "英雄";
        }
        else
        {
            unitName = "士兵";
        }
        damageTypeStr = damageType == DamageType.Critical ? "暴击" : "普通攻击" ;
        damageHp = subHp.ToString();
        Debug.Log(unitName + damageTypeStr + damageHp);
    }

上面列出的第一行代码的意思是向this.unit的OnSubHp事件登记方法OnSubHp的地址,当OnSubHp事件被触发时通知调用OnSubHp方法。而这行代码的意义在于,通过构造SubHpHandler委托类型的实例来获取一个将回调方法OnSubHp进行包装的包装器,以确保回调方法只能以类型安全的方式调用。同时通过这个包装器,我们还获得了对委托链的支持。但是,更多的程序员显然更倾向于简单的表达方式,他们无需真正了解创建委托实例以获得包装器的意义,而只需要为事件注册相应的回调方法即可。例如下面的这行代码:

this.unit.OnSubHp += this.OnSubHp;

之所以能够这样写,我在之前的博客中已经有过解释。虽然“+=”操作符期待的是一个SubHpHandler委托类型的对象,而this.OnSubHp方法应该被SubHpHandler委托类型对象包装起来。但是由于C#的编译器能够自行推断,因而可以将构造SubHpHandler委托实例的代码省略,使得代码对程序员来说可读性更强。不过,编译器在幕后却并没有什么变化,虽然开发者的语法得到了简化,但是编译器生成CIL代码仍旧会创建新的SubHpHandler委托类型实例。

简而言之,C#允许通过指定回调方法的名称而省略构造委托类型实例的代码。

0x02 匿名方法初探

在上一篇博文中,我们可以看到通常在使用委托时,往往要声明相应的方法,例如参数和返回类型必须符合委托类型确定的方法原型。而且,我们在实际的游戏开发过程中,往往也需要委托的这种机制来处理十分简单的逻辑,但对应的,我们必须要创建一个新的方法和委托类型匹配,这样做看起来将会使得代码变得十分臃肿。因而,在C#2的版本中,引入了匿名方法这种机制。什么是匿名方法?下面让我们来看一个小例子。

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

public class DelegateTest : MonoBehaviour {
    
       // Use this for initialization
       void Start () {
              //将匿名方法用于Action<T>委托类型
              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 () {
    
       }
}

将这个DelegateTest脚本挂载在某个游戏场景中的物体上,运行编辑器,可以看到在调试窗口输出了如下内容。

My name is chenjiadong

UnityEngine.Debug:Log(Object)

My age is 26

UnityEngine.Debug:Log(Object)

在解释这段代码之前,我需要先为各位读者介绍一下常见的两个泛型委托类型:Action<T>以及Func<T>。它们的表现形式主要如下:

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);

从Action<T>的定义形式上可以看到。Action<T>是没有返回值得。适用于任何没有返回值的方法。

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>委托的定义是相对于Action<T>来说。Action<T>是没有返回值的方法委托,Func<T>是有返回值的委托。返回值的类型,由泛型中定义的类型进行约束。

好了,各位读者对C#的这两个常见的泛型委托类型有了初步的了解之后,就让我们来看一看上面那段使用了匿名方法的代码吧。首先我们可以看到匿名方法的语法:先使用delegate关键字之后如果有参数的话则是参数部分,最后便是一个代码块定义对委托实例的操作。而通过这段代码,我们也可以看出一般方法体中可以做到事情,匿名函数同样可以做。而匿名方法的实现,同样要感谢编译器在幕后为我们隐藏了很多复杂度,因为在CIL代码中,编译器为源代码中的每一个匿名方法都创建了一个对应的方法,并且采用了和创建委托实例时相同的操作,将创建的方法作为回调函数由委托实例包装。而正是由于是编译器为我们创建的和匿名方法对应的方法,因而这些的方法名都是编译器自动生成的,为了不和开发者自己声明的方法名冲突,因而编译器生成的方法名的可读性很差。

当然,如果乍一看上面的那段代码似乎仍然很臃肿,那么能否不赋值给某个委托类型的实例而直接使用呢?答案是肯定的,同样也是我们最常使用的匿名方法的一种方式,那便是将匿名方法作为另一个方法的参数使用,因为这样才能体现出匿名方法的价值——简化代码。下面就让我们来看一个小例子,还记得List<T>列表吗?它有一个获取Action<T>作为参数的方法——ForEach,该方法对列表中的每个元素执行Action<T>所定义的操作。下面的代码将演示这一点,我们使用匿名方法对列表中的元素(向量Vector3)执行获取normalized的操作。

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 () {
    
       }
}

我们可以看到,一个参数为Vector3的匿名方法:


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

实际上作为参数传入到了List的ForEach方法中。这段代码执行之后,我们可以在Unity3D的调试窗口观察输出的结果。内容如下:

(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)

那么,匿名方法的表现形式能否更加极致的简洁呢?当然,如果不考虑可读性的话,我们还可以将匿名方法写成这样的形式:

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

当然,这里仅仅是给各位读者们一个参考,事实上这种可读性很差的形式是不被推荐的。

除了Action<T>这种返回类型为void的委托类型之外,上文还提到了另一种委托类型,即Func<T>。所以上面的代码我们可以修改为如下的形式,使得匿名方法可以有返回值。

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 () {

       }
}

在匿名方法中,我们使用了return来返回指定类型的值,并且将匿名方法赋值给了Func<T>委托类型的实例。将上面这个C#脚本运行,在Unity3D的调试窗口我们可以看到输出了如下内容:

My name is chenjiadong

UnityEngine.Debug:Log(Object)

26

UnityEngine.Debug:Log(Object)

可以看到,我们通过tellMeYourName和tellMeYourAge这两个委托实例分别调用了我们定义的匿名方法。

当然,在C#语言中,除了刚刚提到过的Action<T>和Func<T>之外,还有一些我们在实际的开发中可能会遇到的预置的委托类型,例如返回值为bool型的委托类型Predicate<T>。它的签名如下:

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

而Predicate<T>委托类型常常会在过滤和匹配目标时发挥作用。下面让我们来再来看一个小例子。

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("英雄的个数为:" + this.heroCount);
              Debug.Log("士兵的个数为:" + this.soldierCount);
       }

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

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

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

       }
}

上面这段代码通过使用Predicate委托类型判断基础单位(BaseUnit)到底是士兵(Soldier)还是英雄(Hero),进而统计列表中士兵和英雄的数量。正如我们刚刚所说的Predicate主要用来做匹配和过滤,那么上述代码运行之后,输出如下的内容:

英雄的个数为:2

UnityEngine.Debug:Log(Object)

士兵的个数为:5

UnityEngine.Debug:Log(Object)

当然除了过滤和匹配目标,我们常常还会碰到对列表按照某一种条件进行排序的情况。例如要对按照英雄的最大血量进行排序或者按照英雄的战斗力来进行排序等等,可以说是按照要求排序是游戏系统开发过程中最常见的需求之一。那么是否也可以通过委托和匿名方法来方便的实现排序功能呢?C#又是否为我们预置了一些便利的“工具”呢?答案仍然是肯定的。我们可以方便的通过C#提供的Comparison<T>委托类型结合匿名方法来方便的为列表进行排序。

Comparison<T>的签名如下:

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

由于Comparison<T>委托类型是IComparison<T>接口的委托版本,因而我们可以进一步来分析一下它的两个参数以及返回值。如下表:

好了,现在我们已经明确了Comparison<T>委托类型的参数和返回值的意义。那么下面我们就通过定义匿名方法来使用它对英雄(Hero)列表按指定的标准进行排序吧。

首先我们重新定义Hero类,提供英雄的属性数据。

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;
              }
       }
}

之后使用Comparison<T>委托类型和匿名方法来对英雄列表进行排序。

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));

              //按英雄的ID排序
              this.SortHeros(bList, delegate(Hero Obj, Hero Obj2){
                     return Obj.id.CompareTo(Obj2.id);
              },"按英雄的ID排序");

              //按英雄的maxHp排序
              this.SortHeros(bList, delegate(Hero Obj, Hero Obj2){
                     return Obj.maxHp.CompareTo(Obj2.maxHp);
              },"按英雄的maxHp排序");

              //按英雄的attack排序
              this.SortHeros(bList, delegate(Hero Obj, Hero Obj2){
                     return Obj.attack.CompareTo(Obj2.attack);
              },"按英雄的attack排序");

              //按英雄的defense排序
              this.SortHeros(bList, delegate(Hero Obj, Hero Obj2){
                     return Obj.defence.CompareTo(Obj2.defence);
              },"按英雄的defense排序");

              //按英雄的powerRank排序
              this.SortHeros(bList, delegate(Hero Obj, Hero Obj2){
                     return Obj.PowerRank.CompareTo(Obj2.PowerRank);
              },"按英雄的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 () {

       }
}

这样,我们可以很方便的通过匿名函数来实现按英雄的ID排序、按英雄的maxHp排序、按英雄的attack排序、按英雄的defense排序以及按英雄的powerRank排序的要求,而无需为每一种排序都单独写一个独立的方法。

未完待续

阅读 4.4k

Runtime
编程首先是爱好,其次才是职业。专注前沿技术,热爱开源。深信代码改变世界。没有值得吹嘘的项目,只有...

Microsoft Visual Studio and Development Technologies MVP

556 声望
61 粉丝
0 条评论

Microsoft Visual Studio and Development Technologies MVP

556 声望
61 粉丝
文章目录
宣传栏