通過上一篇博客《匹夫細說C#:庖丁解牛聊委托,那些編譯器藏的和U3D給的》的內容,我們實現了使用委托來構建我們自己的消息系統的過程。但是在日常的開發中,仍然有很多開發者因為這樣或那樣的原因而選擇疏遠委托,而其中最常見的一個原因便是因為委托的語法奇怪而對委托產生抗拒感。
因而本文的主要目標便是介紹一些委托的簡化語法,為有這種心態的開發者們減輕對委托的抗拒心理。
委托的一種常見的使用方式,就像下面的這行代碼一樣:
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#允許通過指定回調方法的名稱而省略構造委托類型實例的代碼。
在上一篇博文中,我們可以看到通常在使用委托時,往往要聲明相應的方法,例如參數和返回類型必須符合委托類型確定的方法原型。而且,我們在實際的游戲開發過程中,往往也需要委托的這種機制來處理十分簡單的邏輯,但對應的,我們必須要創建一個新的方法和委托類型匹配,這樣做看起來將會使得代碼變得十分臃腫。因而,在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 () { } } 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排序的要求,而無需為每一種排序都單獨寫一個獨立的方法。
好,通過上面的分析,我們可以看到使用了匿名方法之後的確簡化了我們在使用委托時還要單獨聲明對應的回調函數的繁瑣。那麼是否可能更加極致一些,比如用在我們在前面介紹的事件中,甚至是省略參數呢?下面我們來修改一下我們在事件的部分所完成的代碼,看看如何通過使用匿名方法來簡化它吧。
在之前的博客的例子中,我們定義了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)
當然,在使用匿名方法時另一個值得開發者注意的一個知識點便是閉包情況。所謂的閉包指的是:一個方法除了能和傳遞給它的參數交互之外,還可以同上下文進行更大程度的互動。
首先要指出閉包的概念並非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好了,接下來讓我們來分析一下這段代碼中的變量吧。
好了,明白了上面這段代碼中各個變量的含義之後,我們就可以繼續探索匿名方法究竟是如何捕捉外部變量以及捕捉外部變量的意義了。
首先,我們要明確一點,所謂的捕捉變量的背後所發生的操作的確是針對變量而言的,而不是僅僅獲取變量所保存的值。這將導致什麼後果呢?不錯,這樣做的結果是被捕捉的變量的存活周期可能要比它的作用域長,關於這一點我們之後再詳細討論,現在的當務之急是搞清楚匿名方法是如何捕捉外部變量的。
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被我們在匿名方法中捕獲了。正是由於匿名方法捕獲的是變量本身,因而我們才獲得了使用參數的能力,而不是在匿名方法中寫死一個確定的數值來和英雄的攻擊力做比較。這樣在經過設計之後,代碼結構會變得十分精巧。
當然,我們之前還說過將匿名方法賦值給一個委托實例時並不會立刻執行這個匿名方法內部的代碼,而是當這個委托被調用時才會執行匿名方法內部的代碼。那麼一旦匿名方法捕獲了外部變量,就有可能面臨一個十分可能會發生的問題。那便是如果創建了這個被捕獲的外部變量的方法返回之後,一旦再次調用捕獲了這個外部變量的委托實例,那麼會出現什麼情況呢?也就是說,這個變量的生存周期是會隨著創建它的方法的返回而結束呢?還是繼續保持著自己的生存呢?下面我們還是通過一個小例子來一窺究竟。
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這個委托實例,卻看到輸出的結果依次是11、111、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#:不是“棧類型”的值類型,從生命周期聊存儲位置》 也進行過討論,歡迎各位交流指正。