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

this.unit.OnSubHp += this.OnSubHp;

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

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

0x02 匿名方法初探

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

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

      

       }

}
View Code

上面這段代碼經過使用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>接口的委託版本,於是咱們能夠進一步來分析一下它的兩個參數以及返回值。以下表:

參數

類型

做用

x

T

要比較的第一個對象

y

T

要比較的第二個對象

返回值

含義

小於0

x小於y。

等於0

x等於y。

大於0

x大於y。

       

好了,如今咱們已經明確了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;

              }

       }
}
View Code

以後使用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 () {

      

       }

}
View Code

這樣,咱們能夠很方便的經過匿名函數來實現按英雄的ID排序、按英雄的maxHp排序、按英雄的attack排序、按英雄的defense排序以及按英雄的powerRank排序的要求,而無需爲每一種排序都單獨寫一個獨立的方法。

0x03 使用匿名方法省略參數

好,經過上面的分析,咱們能夠看到使用了匿名方法以後的確簡化了咱們在使用委託時還要單獨聲明對應的回調函數的繁瑣。那麼是否可能更加極致一些,好比用在咱們在前面介紹的事件中,甚至是省略參數呢?下面咱們來修改一下咱們在事件的部分所完成的代碼,看看如何經過使用匿名方法來簡化它吧。

在以前的博客的例子中,咱們定義了AddListener來爲BattleInformationComponent 的OnSubHp方法訂閱BaseUnit的OnSubHp事件。

 private void AddListener()
{
    this.unit.OnSubHp += this.OnSubHp;
}

其中this.OnSubHp方法是咱們爲了響應事件而單獨定義的一個方法,若是不定義這個方法而改由匿名方法直接訂閱事件是否能夠呢?答案是確定的。

       private void AddListener()

       {

              this.unit.OnSubHp += delegate(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);

 

              };

       }
View Code

在這裏咱們直接使用了delegate關鍵字定義了一個匿名方法來做爲事件的回調方法而無需再單獨定義一個方法。可是因爲在這裏咱們要實現掉血的信息顯示功能,於是看上去咱們須要全部傳入的參數。那麼在少數狀況下,咱們不須要使用事件所要求的參數時,是否能夠經過匿名方法在不提供參數的狀況下訂閱那個事件呢?答案也是確定的,也就是說在不須要使用參數的狀況下,咱們經過匿名方法能夠省略參數。仍是在觸發OnSubHp事件時,咱們只須要告訴開發者事件觸發便可,因此咱們能夠將AddListener方法改成下面這樣:

private void AddListener()

{

       this.unit.OnSubHp += this.OnSubHp;

       this.unit.OnSubHp += delegate {

              Debug.Log("呼救呼救,我被攻擊了!");

       };

}

以後,讓咱們運行一下修改後的腳本。能夠在Unity3D的調試窗口看到以下內容的輸出:

英雄暴擊10000

UnityEngine.Debug:Log(Object)

呼救呼救,我被攻擊了!

UnityEngine.Debug:Log(Object)

0x04 匿名方法和閉包

固然,在使用匿名方法時另外一個值得開發者注意的一個知識點即是閉包狀況。所謂的閉包指的是:一個方法除了能和傳遞給它的參數交互以外,還能夠同上下文進行更大程度的互動。

首先要指出閉包的概念並不是C#語言獨有的。事實上閉包是一個很古老的概念,而目前不少主流的編程語言都接納了這個概念,固然也包括咱們的C#語言。而若是要真正的理解C#中的閉包,咱們首先要先掌握另外兩個概念:

1.外部變量:或者稱爲匿名方法的外部變量指的是定義了一個匿名方法的做用域內(方法內)的局部變量或參數對匿名方法來講是外部變量。下面舉個小例子,各位讀者可以更加清晰的明白外部變量的含義:

int n = 0;

Del d = delegate() {

Debug.Log(++n);

};

這段代碼中的局部變量n對匿名方法來講是外部變量。

2.捕獲的外部變量:即在匿名方法內部使用的外部變量。也就是上例中的局部變量n在匿名方法內部即是一個捕獲的外部變量。

瞭解了以上2個概念以後,再讓咱們結合閉包的定義,能夠發如今閉包中出現的方法在C#中即是匿名方法,而匿名方法可以使用在聲明該匿名方法的方法內部定義的局部變量和它的參數。而這麼作有什麼好處呢?想象一下,咱們在遊戲開發的過程當中沒必要專門設置額外的類型來存儲咱們已經知道的數據,即可以直接使用上下文信息,這便提供了很大的便利性。那麼下面咱們就經過一個小例子,來看看各類變量和匿名方法的關係吧。

using UnityEngine;

using System;

using System.Collections;

using System.Collections.Generic;

 

public class EnclosingTest : MonoBehaviour {

 

       // Use this for initialization

       void Start () {

              this.EnclosingFunction(999);

       }

      

       // Update is called once per frame

       void Update () {

      

       }

 

       public void EnclosingFunction(int i)

       {

              //對匿名方法來講的外部變量,包括參數i

              int outerValue = 100;

              //被捕獲的外部變量

              string capturedOuterValue = "hello world";

 

              Action<int> anonymousMethod = delegate(int obj) {

                     //str是匿名方法的局部變量

                     //capturedOuterValue和i

                     //是匿名方法捕獲的外部變量

                     string str = "捕獲外部變量" + capturedOuterValue + i.ToString();

                     Debug.Log(str);

              };

              anonymousMethod(0);

 

              if(i == 100)

              {

                     //因爲在這個做用域內沒有聲明匿名方法,

                     //於是notOuterValue不是外部變量

                     int notOuterValue = 1000;

                     Debug.Log(notOuterValue.ToString());

              }

       }

}
View Code

好了,接下來讓咱們來分析一下這段代碼中的變量吧。

  • 參數i是一個外部變量,由於在它的做用域內聲明瞭一個匿名方法,而且因爲在匿名方法中使用了它,於是它是一個被捕捉的外部變量。
  • 變量outerValue是一個外部變量,這是因爲在它的做用域內聲明瞭一個匿名方法,可是和i不一樣的一點是outerValue並無被匿名方法使用,於是它是一個沒有被捕捉的外部變量。
  • 變量capturedOuterValue一樣是一個外部變量,這也是由於在它的做用域內一樣聲明瞭一個匿名方法,可是capturedOuterValue和i同樣被匿名方法所使用,於是它是一個被捕捉的外部變量。
  • 變量str不是外部變量,一樣也不是EnclosingFunction這個方法的局部變量,相反它是一個匿名方法內部的局部變量。
  • 變量notOuterValue一樣不是外部變量,這是由於在它所在的做用域中,並無聲明匿名方法。

好了,明白了上面這段代碼中各個變量的含義以後,咱們就能夠繼續探索匿名方法到底是如何捕捉外部變量以及捕捉外部變量的意義了。

0x05 匿名方法如何捕獲外部變量

首先,咱們要明確一點,所謂的捕捉變量的背後所發生的操做的確是針對變量而言的,而不是僅僅獲取變量所保存的值。這將致使什麼後果呢?不錯,這樣作的結果是被捕捉的變量的存活週期可能要比它的做用域長,關於這一點咱們以後再詳細討論,如今的當務之急是搞清楚匿名方法是如何捕捉外部變量的。

using UnityEngine;

using System;

using System.Collections;

using System.Collections.Generic;

 

public class EnclosingTest : MonoBehaviour {

 

       // Use this for initialization

       void Start () {

              this.EnclosingFunction(999);

       }

      

       // Update is called once per frame

       void Update () {

      

       }

 

       public void EnclosingFunction(int i)

       {

              int outerValue = 100;

              string capturedOuterValue = "hello world";

 

              Action<int> anonymousMethod = delegate(int obj) {

                     string str = "捕獲外部變量" + capturedOuterValue + i.ToString();

                     Debug.Log(str);

                     capturedOuterValue = "你好世界";

              };

              capturedOuterValue = "hello world 你好世界";

 

              anonymousMethod(0);

 

              Debug.Log(capturedOuterValue);

       }

}
View Code

將這個腳本掛載在遊戲物體上,運行Unity3D能夠在調試窗口看到以下的輸出內容:

捕獲外部變量hello world 你好世界999

UnityEngine.Debug:Log(Object)

你好世界

UnityEngine.Debug:Log(Object)

可這究竟有什麼特殊的呢?看上去程序很天然的打印出了咱們想要打印的內容。不錯,這段代碼向咱們展現的不是打印出的到底是什麼,而是咱們這段代碼從始自終都是在對同一個變量capturedOuterValue進行操做,不管是匿名方法內部仍是正常的EnclosingFunction方法內部。接下來讓咱們來看看這一切到底是如何發生的,首先咱們在EnclosingFunction方法內部聲明瞭一個局部變量capturedOuterValue而且爲它賦值爲hello world。接下來,咱們又聲明瞭一個委託實例anonymousMethod,同時將一個內部使用了capturedOuterValue變量的匿名方法賦值給委託實例anonymousMethod,而且這個匿名方法還會修改被捕獲的變量的值,須要注意的是聲明委託實例的過程並不會執行該委託實例。於是咱們能夠看到匿名方法內部的邏輯並無當即執行。好了,下面咱們這段代碼的核心部分要來了,咱們在匿名方法的外部修改了capturedOuterValue變量的值,接下來調用anonymousMethod。咱們經過打印的結果能夠看到capturedOuterValue的值已經在匿名方法的外部被修改成了「hello world 你好世界」,而且被反映在了匿名方法的內部,同時在匿名方法內部,咱們一樣將capturedOuterValue變量的值修改成了「你好世界」。委託實例返回以後,代碼繼續執行,接下來會直接打印capturedOuterValue的值,結果爲「你好世界」。這便證實了經過匿名方法建立的委託實例不是讀取變量,而且將它的值再保存起來,而是直接操做該變量。可這究竟有什麼意義呢?那麼,下面咱們就舉一個例子,來看看這一切究竟會爲咱們在開發中帶來什麼好處。

仍舊回到咱們開發遊戲的情景之下,假設咱們須要將一個英雄列表中攻擊力低於10000的英雄篩選出來,而且將篩選出的英雄放到另外一個新的列表中。若是咱們使用List<T>,則經過它的FindAll方法即可以實現這一切。可是在匿名方法出現以前,使用FindAll方法是一件十分繁瑣的事情,這是因爲咱們要建立一個合適的委託,而這個過程十分繁瑣,已經使FindAll方法失去了簡潔的意義。於是,隨着匿名方法的出現,咱們能夠十分方便的經過FindAll方法來實現過濾攻擊力低於10000的英雄的邏輯。下面咱們就來試一試吧。

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<Hero> list1 = new List<Hero>();

              list1.Add(new Hero(1, 1000f, 50f, 100f));

              list1.Add(new Hero(2, 1200f, 20f, 123f));

              list1.Add(new Hero(5, 800f, 100f, 125f));

              list1.Add(new Hero(3, 600f, 54f, 120f));

              list1.Add(new Hero(4, 2000f, 5f, 110f));

              list1.Add(new Hero(6, 3000f, 65f, 105f));

 

              List<Hero> list2 = this.FindAllLowAttack(list1, 50f);

              foreach(Hero hero in list2)

              {

                     Debug.Log("hero's attack :" + hero.attack);

              }

       }

 

       private List<Hero> FindAllLowAttack(List<Hero> heros, float limit)

       {

              if(heros == null)

                     return null;

              return heros.FindAll(delegate(Hero obj) {

                     return obj.attack < limit;

              });

       }

 

       // Update is called once per frame

       void Update () {

      

       }

}
View Code

看到了嗎?在FindAllLowAttack方法中傳入的float類型的參數limit被咱們在匿名方法中捕獲了。正是因爲匿名方法捕獲的是變量自己,於是咱們纔得到了使用參數的能力,而不是在匿名方法中寫死一個肯定的數值來和英雄的攻擊力作比較。這樣在通過設計以後,代碼結構會變得十分精巧。

0x06 局部變量的存儲位置

固然,咱們以前還說過將匿名方法賦值給一個委託實例時並不會馬上執行這個匿名方法內部的代碼,而是當這個委託被調用時纔會執行匿名方法內部的代碼。那麼一旦匿名方法捕獲了外部變量,就有可能面臨一個十分可能會發生的問題。那即是若是建立了這個被捕獲的外部變量的方法返回以後,一旦再次調用捕獲了這個外部變量的委託實例,那麼會出現什麼狀況呢?也就是說,這個變量的生存週期是會隨着建立它的方法的返回而結束呢?仍是繼續保持着本身的生存呢?下面咱們仍是經過一個小例子來一窺究竟。

using UnityEngine;

using System;

using System.Collections;

using System.Collections.Generic;

 

public class DelegateTest : MonoBehaviour {

 

       // Use this for initialization

       void Start () {

              Action<int> act = this.TestCreateActionInstance();

              act(10);

              act(100);

              act(1000);

       }

 

       private Action<int> TestCreateActionInstance()

       {

              int count = 0;

              Action<int> action = delegate(int number) {

                     count += number;

                     Debug.Log(count);

              };

              action(1);

              return action;

       }

 

       // Update is called once per frame

       void Update () {

      

       }

}
View Code

將這個腳本掛載在Unity3D場景中的某個遊戲物體上,以後啓動遊戲,咱們能夠看到在調試窗口的輸出內容以下:

1

UnityEngine.Debug:Log(Object)

11

UnityEngine.Debug:Log(Object)

111

UnityEngine.Debug:Log(Object)

1111

UnityEngine.Debug:Log(Object)

若是看到這個輸出結果,各位讀者是否會感到一絲驚訝呢?由於第一次打印出1這個結果,咱們十分好理解,由於在TestCreateActionInstance方法內部咱們調用了一次action這個委託實例,而其局部變量count此時固然是可用的。可是以後當TestCreateActionInstance已經返回,咱們又三次調用了action這個委託實例,卻看到輸出的結果依次是十一、1十一、111,是在同一個變量的基礎上累加而獲得的結果。可是局部變量不是應該和方法同樣分配在棧上,一旦方法返回便會隨着TestCreateActionInstance方法對應的棧幀一塊兒被銷燬嗎?可是,當咱們再次調用委託實例的結果卻表示,事實並不是如此。TestCreateActionInstance方法的局部變量count並無被分配在棧上,相反,編譯器事實上在幕後爲咱們建立了一個臨時的類用來保存這個變量。若是咱們查看編譯後的CIL代碼,可能會更加直觀一些。下面即是這段C#代碼對應的CIL代碼。

.class nested private auto ansi sealed beforefieldinit '<TestCreateActionInstance>c__AnonStorey0'

     extends [mscorlib]System.Object

  {

    .custom instance void class [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::'.ctor'() =  (01 00 00 00 ) // ....

 

    .field  assembly  int32 count

 

    // method line 5

    .method public hidebysig specialname rtspecialname

           instance default void '.ctor' ()  cil managed

    {

        // Method begins at RVA 0x20c1

       // Code size 7 (0x7)

       .maxstack 8

       IL_0000:  ldarg.0

       IL_0001:  call instance void object::'.ctor'()

       IL_0006:  ret

    } // end of method <TestCreateActionInstance>c__AnonStorey0::.ctor

 

   ...

 

  } // end of class <TestCreateActionInstance>c__AnonStorey0

咱們能夠看到這個編譯器生成的臨時的類的名字叫作'<TestCreateActionInstance>c__AnonStorey0',這是一個讓人看上去十分奇怪,可是識別度很高的名字,咱們以前已經介紹過編譯器生成的名字的特色,這裏就不贅述了。仔細來分析這個類,咱們能夠發現TestCreateActionInstance這個方法中的局部變量count此時是編譯器生成的類'<TestCreateActionInstance>c__AnonStorey0'的一個字段:

.field  assembly  int32 count

這也就證實了TestCreateActionInstance方法的局部變量count此時被存放在另外一個臨時的類中,而不是被分配在了TestCreateActionInstance方法對應的棧幀上。那麼TestCreateActionInstance方法又是如何來對它的局部變量count執行操做呢?答案其實十分簡單,那就是TestCreateActionInstance方法保留了對那個臨時類的一個實例的引用,經過類型的實例進而操做count變量。爲了證實這一點,咱們一樣能夠查看一下TestCreateActionInstance方法對應的CIL代碼。

.method private hidebysig

           instance default class [mscorlib]System.Action`1<int32> TestCreateActionInstance ()  cil managed

    {

        // Method begins at RVA 0x2090

       // Code size 35 (0x23)

       .maxstack 2

       .locals init (

              class DelegateTest/'<TestCreateActionInstance>c__AnonStorey0' V_0,

              class [mscorlib]System.Action`1<int32>      V_1)

       IL_0000:  newobj instance void class DelegateTest/'<TestCreateActionInstance>c__AnonStorey0'::'.ctor'()

       IL_0005:  stloc.0

       IL_0006:  ldloc.0

       IL_0007:  ldc.i4.0

       IL_0008:  stfld int32 DelegateTest/'<TestCreateActionInstance>c__AnonStorey0'::count

       IL_000d:  ldloc.0

       IL_000e:  ldftn instance void class DelegateTest/'<TestCreateActionInstance>c__AnonStorey0'::'<>m__0'(int32)

       IL_0014:  newobj instance void class [mscorlib]System.Action`1<int32>::'.ctor'(object, native int)

       IL_0019:  stloc.1

       IL_001a:  ldloc.1

       IL_001b:  ldc.i4.1

       IL_001c:  callvirt instance void class [mscorlib]System.Action`1<int32>::Invoke(!0)

       IL_0021:  ldloc.1

       IL_0022:  ret

    } // end of method DelegateTest::TestCreateActionInstance

咱們能夠發如今IL_0000行,CIL代碼建立了DelegateTest/'<TestCreateActionInstance>c__AnonStorey0'類的實例,而以後使用count則所有要經過這個實例。一樣,委託實例之因此能夠在TestCreateActionInstance方法返回以後仍然可使用count變量,也是因爲委託實例一樣引用了那個臨時類的實例,而count變量也和這個臨時類的實例一塊兒被分配在了託管堆上而不是像通常的局部變量同樣被分配在棧上。所以,並不是全部的局部變量都是隨方法一塊兒被分配在棧上的,在使用閉包和匿名方法時必定要注意這一個很容易讓人忽視的知識點。固然,關於如何分配存儲空間這個問題,我以前在博文《匹夫細說C#:不是「棧類型」的值類型,從生命週期聊存儲位置》 也進行過討論,歡迎各位交流指正。

相關文章
相關標籤/搜索