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

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

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

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

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

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

       };
}

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

private void AddListener()
{
   this.unit.OnSubHp += this.OnSubHp;
   this.unit.OnSubHp += delegate {
          Debug.Log("呼救呼救,我被攻擊了!");
   };
}

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

英雄暴擊10000

UnityEngine.Debug:Log(Object)

呼救呼救,我被攻擊了!

UnityEngine.Debug:Log(Object)

0x04 匿名方法和閉包

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

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

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

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

  • 參數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);
       }
}

將這個腳本掛載在遊戲物體上,運行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 () {

       }
}

看到了嗎?在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 () {

       }
}

將這個腳本掛載在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#:不是「棧類型」的值類型,從生命週期聊存儲位置》 也進行過討論,歡迎各位交流指正。

相關文章
相關標籤/搜索