亚洲香蕉成人av网站在线观看_欧美精品成人91久久久久久久_久久久久久久久久久亚洲_热久久视久久精品18亚洲精品_国产精自产拍久久久久久_亚洲色图国产精品_91精品国产网站_中文字幕欧美日韩精品_国产精品久久久久久亚洲调教_国产精品久久一区_性夜试看影院91社区_97在线观看视频国产_68精品久久久久久欧美_欧美精品在线观看_国产精品一区二区久久精品_欧美老女人bb

首頁 > 學院 > 開發設計 > 正文

匹夫細說C#:委托的簡化語法,聊聊匿名方法和閉包

2019-11-14 14:03:39
字體:
來源:轉載
供稿:網友

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 () {             }}
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("呼救呼救,我被攻擊了!");       };}

之后,讓我們運行一下修改后的腳本??梢栽赨nity3D的調試窗口看到如下內容的輸出:

英雄暴擊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這個委托實例,卻看到輸出的結果依次是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#:不是“棧類型”的值類型,從生命周期聊存儲位置》 也進行過討論,歡迎各位交流指正。


發表評論 共有條評論
用戶名: 密碼:
驗證碼: 匿名發表
亚洲香蕉成人av网站在线观看_欧美精品成人91久久久久久久_久久久久久久久久久亚洲_热久久视久久精品18亚洲精品_国产精自产拍久久久久久_亚洲色图国产精品_91精品国产网站_中文字幕欧美日韩精品_国产精品久久久久久亚洲调教_国产精品久久一区_性夜试看影院91社区_97在线观看视频国产_68精品久久久久久欧美_欧美精品在线观看_国产精品一区二区久久精品_欧美老女人bb
狠狠综合久久av一区二区小说| 狠狠躁18三区二区一区| 欧美视频免费在线| 精品国偷自产在线视频| 91av视频在线观看| 亚洲欧美另类国产| 欧美国产日韩一区二区在线观看| 九九热最新视频//这里只有精品| 欧美在线观看日本一区| 国产剧情久久久久久| 国产69精品久久久久9999| 日韩视频在线观看免费| 亚洲免费一在线| 精品久久久久久中文字幕一区奶水| 欧美电影在线观看网站| 亚洲色图50p| 91精品综合久久久久久五月天| 91在线精品视频| 91麻豆国产语对白在线观看| 久久久亚洲天堂| 久久亚洲综合国产精品99麻豆精品福利| 日韩免费观看视频| 日韩最新中文字幕电影免费看| 国产999精品久久久影片官网| 欧美视频13p| 日韩中文字幕av| 九九热r在线视频精品| 国内精品久久影院| 国产精品夫妻激情| 久久久国产一区二区三区| 国产精品亚洲网站| 影音先锋日韩有码| 亚洲色图狂野欧美| 这里精品视频免费| 国产成人精品视频在线| 国产成人精品视频在线| 日韩国产高清污视频在线观看| 亚洲国产欧美在线成人app| 主播福利视频一区| 亚洲精品一区二区三区婷婷月| 久久久久久久999| 欧美在线视频网| 成人激情综合网| 亚洲精品福利在线观看| 日韩免费在线免费观看| 欧美成人全部免费| 136fldh精品导航福利| 久久人人爽人人爽人人片av高请| 欧美日韩另类视频| 91欧美精品成人综合在线观看| 日韩美女主播视频| 国产美女扒开尿口久久久| 欧美精品一二区| 91视频九色网站| 国产一区二区在线播放| 国产精品国产三级国产aⅴ浪潮| 一区国产精品视频| 中文字幕av一区中文字幕天堂| 中文字幕亚洲无线码在线一区| 国产精品久久色| 操人视频在线观看欧美| 国产精品入口免费视频一| www.久久久久久.com| 久久99精品久久久久久青青91| 国产精品日韩精品| 久久频这里精品99香蕉| 亚洲精品一二区| 久久久久国产精品免费| 亚洲欧美中文在线视频| 国产成人亚洲综合青青| 日韩久久免费电影| 疯狂蹂躏欧美一区二区精品| 成人av资源在线播放| 国产欧美精品va在线观看| 日本aⅴ大伊香蕉精品视频| 日韩精品电影网| 国产综合久久久久| 久久久中文字幕| 欧美精品www在线观看| 麻豆国产精品va在线观看不卡| 国产久一一精品| 亚洲va码欧洲m码| 北条麻妃99精品青青久久| 亚洲免费影视第一页| 中文字幕欧美精品在线| 国产日产久久高清欧美一区| 亚洲男人天天操| 欧美性猛交xxxx偷拍洗澡| 国产精品999| 欧美天堂在线观看| 97久久精品视频| 国产亚洲日本欧美韩国| 日韩在线免费高清视频| 国产成人jvid在线播放| 亚洲一区二区三区777| 久久在线观看视频| 亚洲自拍偷拍视频| 亚洲国产成人在线播放| 日韩中文娱乐网| 国产精品久久久久久久久粉嫩av| 91国产精品视频在线| 九色精品美女在线| 欧美激情在线狂野欧美精品| 亚洲午夜久久久久久久| 亚洲人成在线播放| 日韩av在线资源| 97久久国产精品| 九九热最新视频//这里只有精品| xxxx欧美18另类的高清| 亚洲欧美综合区自拍另类| 青青久久av北条麻妃黑人| 一本色道久久88综合亚洲精品ⅰ| 欧美日产国产成人免费图片| 成人激情视频在线| 国产欧美日韩精品专区| 国外成人免费在线播放| 色悠悠久久久久| 国产69精品99久久久久久宅男| 永久免费毛片在线播放不卡| 最近中文字幕日韩精品| 日韩精品视频在线观看免费| 日韩成人性视频| 九九热r在线视频精品| 欧美一区二区大胆人体摄影专业网站| 亚洲成av人影院在线观看| 亚洲视频axxx| 欧美视频不卡中文| 久久国产精品电影| 欧美午夜精品久久久久久人妖| 亚洲国产精品va在线看黑人动漫| 欧美日本在线视频中文字字幕| 精品福利在线视频| 隔壁老王国产在线精品| www国产亚洲精品久久网站| 日韩av网站导航| 国产精品一区二区久久国产| 国产精品6699| 亚洲男子天堂网| 91在线无精精品一区二区| 在线看福利67194| 国产精品久久久久久久久久三级| 日本精品视频网站| 亚洲第一国产精品| 国产美女高潮久久白浆| 91在线视频免费| 深夜精品寂寞黄网站在线观看| 欧美成人亚洲成人| 国产mv免费观看入口亚洲| 午夜精品久久久久久99热软件| 国产精品1区2区在线观看| 国产欧美亚洲精品| 欧美孕妇性xx| 欧美大片第1页| 91久久久久久国产精品| 国产欧美精品日韩精品| 国产精品亚洲激情| 日韩激情第一页| 91久久精品国产| 美乳少妇欧美精品| 欧美激情亚洲另类| 亚洲精品丝袜日韩| 欧美另类极品videosbest最新版本| 亚洲大尺度美女在线| 丰满岳妇乱一区二区三区|