1. 程式人生 > >Unity&C#的委託事件總結

Unity&C#的委託事件總結

吐血整理。。基本涵蓋了常見的各種委託,下面放了程式碼可供參考

基礎知識:
釋出器(publisher) 是一個包含事件和委託定義的物件。事件和委託之間的聯絡也定義在這個物件中。釋出器(publisher)類的物件呼叫這個事件,並通知其他的物件。即下文中的Mom(釋出方) 訂閱器(subscriber) 是一個接受事件並提供事件處理程式的物件。在釋出器(publisher)類中的委託呼叫訂閱器(subscriber)類中的方法(事件處理程式)。即下文中的Mom,Dad(監聽方) 發出者sender:用於記錄呼叫事件的是哪個類 注意:我們通常將委託類委託類的物件都成為委託,但是兩者是有區別的。一旦定義了委託類,基本上就可以例項化它的例項,在這一點上和普通類似一致的。即我們也可以有委託陣列
  1. UnityAction本質上是delegate,且有數個泛型版本(引數最多是4個),一個UnityAction可以新增多個函式(多播委託)

  2. UnityEvent本質上是繼承自UnityEventBase的類,它的AddListener()方法能夠註冊UnityAction,是臨時的
  3. RemoveListener能夠取消註冊UnityAction,
  4. 還有Invoke()方法能夠一次性呼叫所有註冊了的UnityAction。
  5. UnityEvent也有數個泛型版本(引數最多也是4個),但要注意的一點是,UnityAction的所有帶引數的泛型版本都是抽象類(abstract),所以如果要使用的話,需要自己宣告一個類繼承之,然後再例項化該類才可以使用。

Unity中通過面板中新增的Listener和通過指令碼新增的Listener實際上是兩種不同型別的Listener:

  1. 在指令碼中通過AddListener()新增的是一個0個引數的delegate(UnityAction)回撥。是不可序列化的,在Inspector中是無法看到的。這種Listener是常規Listener。
  2. 在Inspector中新增的則是永久性的Listener(persistent listener)。他們需要指定GameObject、方法以及方法需要的引數。他們是序列化的,用指令碼是無法訪問到的。
技巧:可以給方法寫不會用到的引數,從而把更多方法註冊給委託 比如如果想將一個方法註冊給一個委託,但是這個方法又沒有相應的引數,那麼就可以加無用引數
Action<T>和Func<T>委託        我們之前都是根據返回值型別和引數列表來定義委託類,然後在根據委託類來生成委託的例項。現在我們還可以使用泛型委託類Action<T>和Func<T>。        泛型Action<T>委託類表示引用一個void返回型別的方法,可以傳遞至多16個不同的引數型別。 Action<in T1,in T2, …,in Tn> (n最大為16,例如Action<in T1,in T2>就表示呼叫2個引數的方法)。Func<T>委託的使用方式和Action<T>委託類 似.Func<T>允許呼叫帶有返回值的方法。Func<in T1, in T2, ...,in Tn, out TResult> (n的最大值還是16,Func<in T1, in T2, out TResult>表示呼叫兩個引數的方法且返回值型別為TResult)。 我們用Func<T>委託來實現上述委託: Func<int, int, int> TestDele = add;       這一條語句就等價於委託類的宣告和委託物件的建立。同理,Action<T>也是一樣的用法。而且功能上沒有任何的不同。唯一不足之處就是引數的個數是有限制的,不過大多數的情況下16個的引數已經足夠使用了。

1、匿名委託

       在這之前我們使用委託那麼都必須先有一個方法。那麼現在我們可以通過另一種方式使委託工作:匿名方法。用匿名方法來實現委託和之前的定義並沒有太大的區別,唯一不同之處就在於例項化。我們就以之前Calc委託類為例:
Calc TestDele = delegate(int a,int b){        //程式碼塊(因為Calc委託類是有返回值的,所以函式體內必須有return語句)};      //這裡有一個分號,千萬不能漏
      通過使用匿名方法,由於不必建立單獨的方法,因此減少了例項化委託所需的編碼系統開銷。而且使用匿名方法可以有效減少要編寫的程式碼,有助於降低程式碼的複雜度。
      然而我們在使用匿名委託的時候我們要遵守兩個原則:1、匿名方法中不能有跳轉語句(break, goto或continue)跳轉到匿名方法的外部,反之,外部程式碼也不能跳轉到該匿名方法內部。2、在匿名方法中不能訪問不安全程式碼。
注意:不能訪問在匿名方法外部使用的ref和out引數。

2、Lambda表示式

      由於Lambda表示式的出現使得我們的程式碼可以變得更加的簡潔明瞭。我們現在來看看Lambda表示式的使用語法:
[委託類] [委託物件名] = ( [引數列表] ) => { /*程式碼塊*/ };      //結尾還是有一個分號
       我們值得注意的是lambda表示式的引數列表,我們只需給出變數名即可,其餘的編譯器會自動和委託類進行匹配。如果委託類使用返回值的,那麼程式碼塊就需要return一個返回值。我們用一個例子來說明上述問題:
Func<int, int> TestLam = (x) => { return x*x; };
       在這裡我們是使用一個引數的為例,上面的寫法是Lambda表示式的正常寫法,但是當引數只有一個時,x兩邊的括號就可以去除,那麼現在程式碼就變成這樣了:
Func<int, int> TestLam = x => { return x*x; };
        當Lambda表示式程式碼塊中只有一條語句,那麼我們就可以把花括號丟了。如果這一條語句還是包含return的語句,那麼我們在去除花括號的同時,必須將return同時刪去。現在上述程式碼就變成了這樣:
Func<int, int> TestLam = x => x*x;
注意:Lambda表示式可以用於型別為委託的任意地方。
3、閉包        通過lambda表示式可以訪問lambda表示式外部的變數,於是我們就引出了一個新的概念-----閉包。我們來看一個例子: int someVal = 5;Func<int, int> f = x => x+someVal;       現在我們很容易知道f(3)的返回值是8,我們繼續:
someVal = 7;
       我們現在將someVal的值改為7,那麼這時我們在呼叫f(3),現在就會很神奇的發現f(3)的返回值變成了10。這就是閉包的 特點,這個特點在程式設計上很大程度上能給我們帶來一定的好處。但是有利終有弊,如果我們使用不當,那麼這就變成了一個非常危險的功能。
       我現在再來看看在foreach語句中的閉包,我們現在看看下面這段程式碼: List<int> values = new List<int>() { 10, 20, 30 }; var funcs = new List<Func<int>>(); foreach (var val in values) {   funcs.Add(() => val);} foreach (var f in funcs) {   Console.WriteLine(f());}         用我們剛才的知識來判斷的話,輸出結果應該是3個30。然而在C#4.0確實是這樣,然而C#5.0會在foreach建立的while迴圈的程式碼塊中建立一個不同的區域性迴圈變數,所以這時在C#5.0中我們輸出的結果應該是分別輸出10,20和30。
using UnityEngine;
using System;//EventArgs需要
using UnityEngine.Events;//Unity事件需要
using UnityEngine.UI;//展示註冊已有的UI事件
public class Mom : MonoBehaviour {
       //本程式碼是以總結順序寫的,宣告-定義-註冊-呼叫。
       //用法1:一個物件可以一口氣執行多個方法,並且可順序執行其他類的方法,用於監聽-執行
       //1.宣告一個C#委託協定,指定方法簽名
       public delegate int MyDelegate(int i, int j);
       //委託也是一個型別,需要宣告物件
       MyDelegate myDelegate;
       //2.C#事件的宣告:event+委託+事件名
       public event MyDelegate CSharpEvent;
       //事件就是一個委託物件,而委託可以是一種型別
       //3.C#自身提供了一種比較好的委託型別:EventHandler
       //sender表示事件源,e表示事件相關的資訊
       public delegate void EventHandler(object sender, EventArgs e);
       EventHandler CSharpDelegateHandler;
       //4.另一種更好的委託方式是使用泛型引數的委託型別:EventHandler<TEventArgs>,其簽名如下:
       //如果產生額外引數,第二個引數是從 EventArgs 派生的型別並提供所有欄位或屬性需要儲存事件資料。使用 EventHandler<TEventArgs> 的優點在於,如果事件生成事件資料,則無需編寫自己的自定義委託程式碼。
       public delegate void EventHandler<TEventArgs>(object sender, TEventArgs e);
       //5.我們也可以為這些委託宣告事件(EventHandler事件處理器就是事件)
       public event EventHandler CSharpEventHandler;
       //6.使用UnityEvent(可視) 和 UnityAction(不可視)
       //使用類建立的話一條語句就等價於委託類的宣告和委託物件的建立
       public UnityAction unityAction;//本質是delegate
       public UnityEvent unityEvent = new UnityEvent();//也可以在定義時new。本質上是繼承自UnityEventBase的類
       
       //7.Unity泛型委託
       //因為UnityEvent<T0>是抽象類,所以需要宣告一個類來繼承它
       public class UnityActionWithParameter: UnityEvent<int>{
              //巢狀類
       }
       public UnityActionWithParameter unityActionWithParameter = new UnityActionWithParameter();
       public UnityAction<int> unityIntAction;
       public UnityEvent<int,int> unityIntEvent;
       //8.泛型委託Action<T>和Func<T>
       public Action<int, int> CSharpIntAction;//使用類建立的話一條語句就等價於委託類的宣告和委託物件的建立
       public Func<int, int, int> CSharpIntFunc;//最後一個int是函式返回型別
       //註冊的方法要求:返回型別+引數的型別+引數個數     引數名可以不同
       int Add(int a, int b)
       {
              return a + b;
       }
       int Subtarct(int a,int b)
       {
              return a - b;
       }
       void NoParameter()
       {
              //無參方法
       }
       void OneIntParameter(int i)
       {
              //一個int引數,且返回void
       }
       void TwoIntParameter(int a,int b)
       {
              //2個int引數,且返回void
       }
       //針對EventHandler的註冊方法
       void OnGameStart(object sender, EventArgs e)
       {
              //你要執行的程式碼
       }
       //用法2:委託充當函式指標
       static void functionPointer(MyDelegate function,int a,int b)
       {
              function(a, b);
       }
       void Start()
       {
              //委託物件的初始化和賦值 和普通類一致。注意由於宣告的委託不包含0參建構函式,所以必須要註冊引數
              myDelegate =new MyDelegate(Add);
              //C#事件新增方法
              CSharpEvent += new MyDelegate(Add);
              //其實這幾個無論用=,+=都是可以的
              CSharpDelegateHandler = new EventHandler(OnGameStart);
              //另外還有其他的註冊方式,這裡就拿C#標準委託的事件為例
              CSharpEventHandler += OnGameStart;
              CSharpEventHandler -= OnGameStart;//當監聽到後不需要執行這個方法時(取消監聽)
              CSharpEvent += Add;//同樣可以
              Dad dad = new Dad();
              CSharpEvent += dad.Add;//如果我們想註冊外部類的方法也是可以的(注意要public)
              unityAction = new UnityAction(NoParameter);
              unityAction += NoParameter;
              //注意UnityEvent只能用AddListener臨時註冊方法
              unityEvent.AddListener(NoParameter);
              unityEvent.RemoveListener(NoParameter);
              unityIntAction += OneIntParameter;//可以使用+=
              unityIntAction = new UnityAction<int>(OneIntParameter);//註冊要求一個int引數,且返回void
              unityActionWithParameter.AddListener(OneIntParameter);//其實我不確定上面那個能不能用,例項中是隻用了這行的事件的
              unityIntEvent.AddListener(TwoIntParameter);//註冊要求2個int引數,且返回void
              CSharpIntAction += TwoIntParameter;//可以使用+=
              CSharpIntAction = new Action<int, int>(TwoIntParameter);
              CSharpIntFunc += Add;//要求返回int型別
              CSharpIntFunc = new Func<int, int, int>(Add);
              //9.lambda方式可以新增包含任意引數的函式,非常方便。以下是為onClick事件註冊方法
              GetComponent<Button>().onClick.AddListener(() =>
              {//意思是用(引數列表)執行以下函式體的內容
               //你要執行的程式碼,也可以執行方法,可以使用引數
              });
              //以下是呼叫。一般放在監聽成功的位置
              //C#委託的呼叫:委託物件及引數
              myDelegate(1, 2);
              myDelegate.Invoke(1,2);//呼叫與上面等價
              //C#事件的呼叫,同委託完全一樣
              CSharpEvent(1, 2);
              CSharpEvent.Invoke(2, 4);
              //C#標準委託呼叫,注意不需任何引數時就這樣
              if(CSharpDelegateHandler!=null)//可以加個安全檢查
                      CSharpDelegateHandler(this, EventArgs.Empty);
              unityAction();//無參呼叫
              unityAction.Invoke();
              unityEvent.Invoke();//UnityEvent只能使用Invoke()呼叫
              unityIntAction(1);//1個int引數
              unityActionWithParameter.Invoke(1);//繼承UnityAction<int>的事件
              unityIntEvent.Invoke(1,2);//在Invoke中提供實參
              CSharpIntAction(1, 2);//Invoke就省略不寫了
              CSharpIntFunc(1, 2);
              //函式指標的使用
              functionPointer(myDelegate, 2, 4);//可以用已有的委託物件做引數
              functionPointer(new MyDelegate(Subtarct), 4, 2);//也可以new一個新物件
              //另外如果物件是public的那麼我們也可以在外部類執行以上程式碼
       }
}


附C++的函式指標用法
//這些不用看 //Platform: WinXP + VC6.0 #include <iostream.h>
#include <list>
using namespace std;

void max(int a, int b)
{
    cout<<"now call max("<<a<<","<<b<<")..."<<endl;
    int t = a>b?a:b;
    cout<<t<<endl;
}
void min(int a, int b)
{
    cout<<"now call min("<<a<<","<<b<<")..."<<endl;
    int t = a<b?a:b;
    cout<<t<<endl;
}
typedef void (*myFun)(int a, int b); //定義一個函式指標用來引用max,min

//從這裡開始看
//回撥函式
void callback(myFun fun, int a, int b)
{也就是說,方法名成了一個引數fun,fun可以表示任何方法
    fun(a,b);
}
void main()
{
    int i = 10;
    int j = 55;
    callback(max,i,j);

    callback(min,i,j);
}

Output: now call max(10,55)...
55
now call min(10,55)... 10