1. 程式人生 > >快速理解C#中的委託與事件

快速理解C#中的委託與事件

以下內容只是個人理解,僅供參考。

什麼是委託?

先看最簡單的委託例子:

namespace DelegateTest
{
    public delegate void MessageDelegate(string name);
    class Program
    {
        private static void SaySomething(string name)
        {
            Console.WriteLine("You said " + name);
        }
        static void Main(string[] args)
        {
            MessageDelegate msgDelegate = new MessageDelegate(SaySomething);
            msgDelegate("Hello");
            Console.ReadKey();
        }
    }
}

輸出結果為:You said Hello

從程式碼中我們可以看到,SaySomething是一個方法,delegate是一個類。

通俗的來說,委託是方法的容器,就像陣列string[]string變數的容器。

如返回值void型別和接收string型別引數的委託物件只能繫結同樣型別的方法。

MessageDelegate msgDelegate = new MessageDelegate(SaySomething);

這句程式碼是初始化委託物件,我們把SaySomething這個方法封裝進去msgDelegate中。

它的構造方法必須要有1個方法作為引數才能初始化。

初始化一個委託物件也可以直接賦值一個方法初始化,

:

MessageDelegate msgDelegate=SaySomething;

這個時候SaySomething方法已經裝入了委託物件msgDelegate內,所以們可以通過呼叫委託物件來呼叫已裝入的方法。

我們要使用的時候就把委託物件msgDelegate當做方法一樣呼叫。


過程總結:

1.定義委託物件

public delegate void MessageDelegate(string name);

2.定義方法

 private static void SaySomething(string name)

        {

            Console.WriteLine("You said "

 + name);

        }

3.新建一個委託物件然後初始化

MessageDelegate msgDelegate = new MessageDelegate(SaySomething);

或者

MessageDelegate msgDelegate=SaySomething;

4.呼叫委託物件

msgDelegate(“Hello”);

以上是不帶返回值的委託,如果要帶返回值的委託可以自己制定:

public delegate int AddNumber(int a,int b);

  static void Main(string[] args)
        {
            AddNumber an= new AddNumber(MyFunc);
           Int n=an(1,2);
Console.WriteLine(n);
           Console.ReadKey();
        }
  private static int AddFunc(int a,int b)
        {
            return a+b;        
}

多路廣播委託:

如果說委託物件是一個容器,那一個委託物件可以繫結多個方法。

很簡單,我們使用+=來繫結更多的物件。

反之,我們可以用-=來取消繫結物件。

namespace DelegateTest
{

    class Program
    {
        public delegate void MessageDelegate(string name);
        private static void SaySomething(string name)
        {
            Console.WriteLine("You said " + name);
        }
        private static void SayAnything(string str)
        {
            Console.WriteLine(str);
        }
        static void Main(string[] args)
        {
            MessageDelegate msgDelegate = new MessageDelegate(SaySomething);
            msgDelegate += SayAnything;
            msgDelegate("Meh");
            Console.ReadKey();
        }
    }
}

輸出結果為:

You said Meh

Meh

注意:委託物件的列表為空(null)的時候不能呼叫,但可以繫結或取消繫結物件。

既然委託物件是一個類,那麼我們也可以把這個委託物件作為一個方法的引數來傳遞。

namespace DelegateTest
{

    class Program
    {
        public delegate void MessageDelegate(string name);
        private static void SaySomething(string name)
        {
            Console.WriteLine("You said " + name);
        }
           private static void SayAnything(MessageDelegate msgDelegate)
        {
            if(msgDelegate!=null)
            msgDelegate("Hello");
        }

        static void Main(string[] args)
        {
            MessageDelegate msgDelegate = new MessageDelegate(SaySomething);
            SayAnything(msgDelegate);
            SayAnything(SaySomething);
            msgDelegate.Invoke("xxx");
            msgDelegate("xxx");
            Console.ReadKey();
        }
    }
}

輸出結果為:

You said Hello

You said Hello

xxx

xxx

我們用了4種不同的方法依次呼叫,第一個是傳入一個委託物件,第二個是傳入方法,第三個是直接呼叫委託,第四個和第三個是等價的。

由此可看出我們可以把方法作為引數傳入一個委託物件型別來呼叫,也可以把一個裝有方法的委託型別作為引數傳遞後呼叫。

總結:

委託物件是一個類,可以傳遞與委託物件返回值和形參相同的方法,然後去呼叫它。

委託可以繫結多個方法然後依次呼叫。

委託是函式指標,主要功能是用來實現類之間的交流和回撥,就像你委託朋友幫你做一件事,你朋友會告訴你進度。

為什麼要用委託?什麼時候用委託?

看了上面的例子,大部分同學應該對委託大概有個理解了,可能有同學會問為什麼不直接呼叫SaySomething而要通過委託物件間接呼叫,用了委託到底有什麼好處,什麼時候該用委託,其實從字面上委託的意思是兩方交流的中介,比如中國和俄羅斯交流需要委託翻譯來實現,接下來我們再看幾個例子。

一、當你不確定使用具體方法的時候

比如你是一所幼兒園老師,你想獎勵小朋友食物,有以下幾種獎勵:

private static void GiveLolipop()
        {
            //給了棒棒糖
            Console.WriteLine("給了棒棒糖");
        }
        private static void GiveCake()
        {
            //給了蛋糕
            Console.WriteLine("給了蛋糕");
        }
        private static void GiveSugar()
        {
            //給了糖果
            Console.WriteLine("給了糖果");
        }
        private static void GiveBiscuit()
        {
            //給了餅乾
            Console.WriteLine("給了餅乾");
        }

如果我們想給每個小孩定製一套獎勵,可以定義多個方法實現

  private static void GiveChildren1()
        {
            GiveSugar();
            GiveBiscuit();
            GiveLolipop();
        }
        private static void GiveChildren2()
        {
            GiveSugar();
            GiveCake();
            GiveBiscuit();
        }
        private static void GiveChildren3()
        {
            GiveLolipop();
            GiveSugar();
            GiveCake();
        }

和使用enumswitch語句

public enum Children{
Jojo,Meme,Kiki
}

private static void GiveChildren(Children children)
{
 switch(children)
            {
                case Children.Jojo:
                    GiveChildren1();
                    break;
                case Children.Meme:
                    GiveChildren2();
                    break;
                        case Children.Kiki:
                    GiveChildren3();
                    break;
                default:
                    GiveSugar();
                    break;
            }
}

雖然這樣做可以解決需求,但是擴充套件性非常差,我們想增加新的一套獎勵就得增加新的方法和修改列舉型別和switch語句。

如果我們使用委託,則可以解決這個問題:

因為可以動態決定使用哪個方法,所以這裡我們不需要列舉。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace DelegateTest
{

    class Program
    {
        public delegate void RewardDelegate();

        static void Main(string[] args)
        {
            RewardDelegate rewardAll,reward1, reward2, reward3;
            rewardAll = GiveSugar;
            rewardAll += GiveBiscuit;
            rewardAll += GiveCake;
            rewardAll += GiveLolipop;
            reward1 = rewardAll - GiveCake;
            reward2 = rewardAll - GiveLolipop;
            reward3 = rewardAll - GiveBiscuit;
            GiveChildren("Jojo", reward2);
            Console.ReadKey();
        }
    
        private static void GiveLolipop()
        {
            //給了棒棒糖
            Console.WriteLine("給了棒棒糖");
        }
        private static void GiveCake()
        {
            //給了蛋糕
            Console.WriteLine("給了蛋糕");
        }
        private static void GiveSugar()
        {
            //給了糖果
            Console.WriteLine("給了糖果");
        }
        private static void GiveBiscuit()
        {
            //給了餅乾
            Console.WriteLine("給了餅乾");
        }
      
        private static void GiveChildren(string name,RewardDelegate rD)
        {
            if (rD != null)
            {
                Console.WriteLine("給" + name + "的獎勵有:");
                rD.Invoke();
            }
        }
    }
}

執行結果如圖:


這個例子可能舉得不太好,但我們可以動態地決定使用哪個方法,程式碼也簡潔了很多,讓程式的擴充套件性更靈活。

所以委託可以讓我們避免大量使用條件語句(if,switch),同時也可以避免程式碼的重複性。

如果還有同學不明白的話,我再舉一個簡單的例子。

假設你需要裝修房子,你需要給裝修工人一個裝修方案,那麼程式碼可以這麼寫:

 public delegate void DecorateDelegate();
        static void Main(string[] args)
        {
            DecorateHouse(ClassicStyle);
            Console.ReadKey();
        }
        private void ClassicStyle()
        {
            Console.WriteLine("你選擇了經典裝修方案");
        }
        private void DecorateHouse(DecorateDelegate dd)
        {
            if (dd != null)
                dd.Invoke();
        }

當你需要更改裝修方案時你只需要更改DecorateHouse裡的引數就可以了,操作起來簡單很多。

二、當你需要實現兩個類之間的溝通時

接下來我們再看最後一個例子:

namespace DelegateTest2
{
    class Program
    {
        static void Main(string[] args)
        {
            Myclass myClass = new Myclass();
            myClass.Working();
        }
    }
    class Myclass
    {
        public void Working()
        {
            for(int i=0;i<10000;i++)
            {
                //處理事件
            }
        }
    }
}


在這樣一個例子中,如果Program類想在迴圈執行的時候想要實時獲得MyclassWorking方法裡的資訊,我們可以使用委託來實現。

class Program
    {
       
        static void Main(string[] args)
        {
            Myclass myClass = new Myclass();
            myClass.Working(CallBack);
        }
        private static void CallBack(int i)
        {
            Console.WriteLine(i);
        }
    }
    class Myclass
    {
        public delegate void CallBack(int i);
        public void Working(CallBack callBack)
        {
            for(int i=0;i<10000;i++)
            {
                //處理事件
                callBack(i);
            }
        }
}

我們把Program類中的CallBack方法傳入了Working方法中,由此得到回撥。

簡單來說就是就是MyClass使用了Program中的CallBack方法來輸出資訊。

別問我為什麼不直接public static 然後直接呼叫,我們考慮的是物件的封裝性。

程式執行結果如圖:



總結:除了將方法作為引數傳遞外,委託的主要功能是實現兩方之間的通訊,然後實現回撥。

對於委託的深入理解

==================================================================

1.委託的協變與逆變

Framework 2.0前的版本,還沒有委託協變的概念

舉個例子:

    public class Human

      {.......}

      public class Man : Human

     {.......}

public delegate Human HumanHandler(int id);

  public delegate Man ManHandler(int id);

在這裡HumanHandler是不能繫結返回值為Man的方法,因為它們被視為兩個不同的型別,雖然Man繼承至Human

而在Framework 2.0後的版本,有了委託協變的概念

我們可以直接繫結返回值為Man的方法:

HumanHandler hh=new HumanHandler(ManHandler);

Man man=hh as Man;

而委託逆變跟委託協變一樣,唯一不同的是它以object作為引數的委託,然後再用is判斷object的型別。

class Program
    {
        public delegate void Handler(object obj);

        public static void GetMessage(object message)
        {
            if (message is string)
                Console.WriteLine("His name is : " + message.ToString());
            if (message is int)
                Console.WriteLine("His age is : " + message.ToString());
        }

        static void Main(string[] args)
        {
            Handler handler = new Handler(GetMessage);
            handler(29);
            Console.ReadKey();
        }
   }

2.泛型委託

對於以上的方法,如果都以object作為引數,每次都要進行拆箱操作是非常消耗效能的,過程也很繁瑣。

因此這裡引入了泛型委託的概念。

  class Program
    {
        public delegate void Handler<T>(T obj);
        static void Main(string[] args)
        {
            Handler<int> handler1 = new Handler<int>(getSquare);
            Handler<string> handler2 = new Handler<string>(sayHi);
            handler1(2);
            handler2("Wix");
            Console.ReadKey();
        }
        static void getSquare(int a)
        {
            Console.WriteLine(a * a);
        }
        static void sayHi(string name)
        {
            
            Console.WriteLine("Hi,"+name);
        }
}

輸出結果如圖:

什麼時候使用泛型委託?

如果你想繫結多個不同型別引數方法的話可以使用泛型委託,而且不需要用is進行型別判斷。這樣我們就可以不用定義多個不同引數型別的委託了。

什麼是事件?

簡單的來說,事件的由來是為了保證系統的封裝性。

上面的程式碼可以看到委託的宣告都是public,這使得外界可以直接進行呼叫或賦值操作。

如果設定成private,我們需要新增AddHandlerRemoveHandler方法(+=-=),就有如getset方法,很麻煩。

所以事件這個概念由此而生。

 public class EventTest
  {
      public delegate void MyDelegate();
      public event MyDelegate MyEvent;
   }
 


*事件對應的變數成員將會被視為 private 變數

如果不明白,可以想象事件是委託的容器,可以保證委託的封裝性。

事件能通過+=-=兩個方式註冊或者登出對其處理的方法

public delegate void MyDelegate(string name);

    public class PersonManager
    {
        public event MyDelegate MyEvent;

        //執行事件
        public void Execute(string name)
        {
            if (MyEvent != null)
                MyEvent(name);
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            PersonManager personManager = new PersonManager();
            //繫結事件處理方法
            personManager.MyEvent += new MyDelegate(GetName);
            personManager.Execute("Leslie");
            Console.ReadKey();
        }

        public static void GetName(string name)
        {
            Console.WriteLine("My name is " + name);
        }
    }

我們也可以直接繫結方法

  personManager.MyEvent += GetName;

或者繫結匿名方法

 personManager.MyEvent += delegate(string name){

                 Console.WriteLine("My name is "+name);

             };

總結:事件就是一個特殊的委託。



什麼時候用事件?

事件可以把邏輯流程拆分成幾個階段,在遊戲中,如單位生產事觸發某個事件,單位死亡時觸發某個事件。

下面是模擬unity中的程式碼例子:

 class UnitHandler
    {

        public delegate void UnitEventHandler(GameObject unit);
        public static event UnitEventHandler onUnitSpawn;
        public static event UnitEventHandler onUnitDestroy;
        public static void NewUnitCreated(GameObject unit)
        {
            if (onUnitSpawn != null)
                onUnitSpawn(unit);
        }
        public static void UnitDead(GameObject unit)
        {
            if(onUnitDestroy!=null)
            onUnitDestroy(unit);
       
        }
}

 class UnitManager
    {
     public void OnEnabled()
        {
            UnitHandler.onUnitSpawn += this.NewUnitCreated;
        }
        public void NewUnitCreated(GameObject unit)
        {
            Console.WriteLine("unit created");
            Console.ReadKey();
        }

      public  void OnDisable()
        {
            UnitHandler.onUnitSpawn -= this.NewUnitCreated;
        }
    }

  class Program
    {
        static void Main(string[] args)
        {
            UnitManager um = new UnitManager();
            um.OnEnabled();
            UnitHandler.NewUnitCreated(new GameObject());
            um.OnDisable();
            UnitHandler.NewUnitCreated(new GameObject());
        }
    }

*這裡的GameObject是一個空類

執行結果為:

unit created

事件可以在某件事件發生時讓一個物件通知另一個物件,可以理解為監聽某個事件,然後在特定條件下觸發。

下面再看一個例子:

在我們建立一個事件之前,我們需要一個委託,而一般標準的委託宣告如下:

    public delegate void EventHandler(object sender, System.EventArgs e);

第一個形參object sender定義了物件來源,第二個形參放的是繼承自System.EventArgs的類,一般上這個類包含了事件的詳細資訊。

例子:

    class ButtonEventArgs:EventArgs

    {

        public string time;

}

在這裡我們不需要傳遞什麼事件資訊,所以我們用基類EventArgs就好。

 public delegate void EventHandler(object sender, System.EventArgs e);

    class Publisher
    {
        public event EventHandler Added; //定義發生事件


        protected virtual void OnAdded(System.EventArgs e) //當事件發生中觸發方法
        {
            if(Added!=null)
            {
                Added(this, e);
            }
        }
        public void Add(object value) //觸發事件的方法
        {
   
            OnAdded(System.EventArgs.Empty);
        }
}



    class Subscriber
    {
        void AddedEventHandler(object sender,System.EventArgs e)
        {
            System.Console.WriteLine("AddEvent occured");
        }
        
        static void Main()
        {
            Subscriber s = new Subscriber();
            Publisher p = new Publisher();
            p.Added += s.AddedEventHandler;
p.Add(10);
        }
}


事件的使用步驟如下:

存放事件的類

1.定義事件

2.觸發事件的方法(protected)

3.間接觸發事件的方法(public)

觸發事件的類

1.定義方法

2.註冊方法

3.觸發方法

再來看最後一個例子:


public class MyEventArgs : EventArgs
    {
        private string args;

        public MyEventArgs(string message)
        {
            args = message;
        }

        public string Message
        {
            get { return args; }
            set { args = value; }
        }
    }

    public class EventManager
    {
        public event EventHandler<MyEventArgs> myEvent;

        public void Execute(string message)
        {
            if (myEvent != null)
                myEvent(this, new MyEventArgs(message));
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            EventManager eventManager = new EventManager();
            eventManager.myEvent += new EventHandler<MyEventArgs>(ShowMessage);
            eventManager.Execute("How are you!");
            Console.ReadKey();
        }

        public static void ShowMessage(object obj,MyEventArgs e)
        {
            Console.WriteLine(e.Message);
        }
    }

這裡我們使用了EventHandler<TEventArgs> 構造出所需要的委託。

public delegate void EventHandler<TEventArgs> (Object sender, TEventArgs e)

MyEventArgs 負責存放事件資訊。

EventManager負責事件的定義和執行。

Program 負責定義方法和事件的觸發。

總結:

委託是派生自System.MultcastDelegate 的類,事件(Event)屬於一種特殊的委託,它與委託型別同步使用。

本文章部分內容參考了此文章並加入了個人理解:

http://www.cnblogs.com/leslies2/archive/2012/03/22/2389318.html