細說C#:委託的簡化語法,聊聊匿名方法和閉包(上)

0x00 前言

經過以前博客《匹夫細說C#:庖丁解牛聊委託,那些編譯器藏的和U3D給的》的內容,咱們實現了使用委託來構建咱們本身的消息系統的過程。可是在平常的開發中,仍然有不少開發者由於這樣或那樣的緣由而選擇疏遠委託,而其中最多見的一個緣由即是由於委託的語法奇怪而對委託產生抗拒感。html

於是本文的主要目標即是介紹一些委託的簡化語法,爲有這種心態的開發者們減輕對委託的抗拒心理。程序員

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進行包裝的包裝器,以確保回調方法只能以類型安全的方式調用。同時經過這個包裝器,咱們還得到了對委託鏈的支持。可是,更多的程序員顯然更傾向於簡單的表達方式,他們無需真正瞭解建立委託實例以得到包裝器的意義,而只須要爲事件註冊相應的回調方法便可。例以下面的這行代碼:ide

this.unit.OnSubHp += this.OnSubHp;

之因此可以這樣寫,我在以前的博客中已經有過解釋。雖然「+=」操做符期待的是一個SubHpHandler委託類型的對象,而this.OnSubHp方法應該被SubHpHandler委託類型對象包裝起來。可是因爲C#的編譯器可以自行推斷,於是能夠將構造SubHpHandler委託實例的代碼省略,使得代碼對程序員來講可讀性更強。不過,編譯器在幕後卻並無什麼變化,雖然開發者的語法獲得了簡化,可是編譯器生成CIL代碼仍舊會建立新的SubHpHandler委託類型實例。函數

簡而言之,C#容許經過指定回調方法的名稱而省略構造委託類型實例的代碼。工具

0x02 匿名方法初探

在上一篇博文中,咱們能夠看到一般在使用委託時,每每要聲明相應的方法,例如參數和返回類型必須符合委託類型肯定的方法原型。並且,咱們在實際的遊戲開發過程當中,每每也須要委託的這種機制來處理十分簡單的邏輯,但對應的,咱們必需要建立一個新的方法和委託類型匹配,這樣作看起來將會使得代碼變得十分臃腫。於是,在C#2的版本中,引入了匿名方法這種機制。什麼是匿名方法?下面讓咱們來看一個小例子。this

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腳本掛載在某個遊戲場景中的物體上,運行編輯器,能夠看到在調試窗口輸出了以下內容。spa

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排序的要求,而無需爲每一種排序都單獨寫一個獨立的方法。

未完待續

相關文章
相關標籤/搜索