C# 委托進階


本文參考自:https://wenku.baidu.com/view/41ab91d3c1c708a1284a44d7.html?qq-pf-to=pcqq.c2c

1、為什么委托定義的返回值通常為void?

盡管並非必須,但是大多數情況委托定義的返回值都為void,因為這部分委托基本都是需要綁定多個方法,也就是當前委托允許多個訂閱者注冊,但是當主函數執行委托對象上注冊的方法時,不會返回結果,只會返回最后一個方法的結果值,這一點可以通過調試下面的代碼就可以看出,代碼如下:

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

namespace Delegate
{
    public delegate int?  OpearteEventHandler(int? a,int? b);
    class Program
    {
        static void Main(string[] args)
        {
            Program p = new Program();
            int? a = 10;
            int? b = 6;
            OpearteEventHandler ah = p.Add;
            ah += p.Sub;
            ah += p.Multiply;
            p.ShowResult(a, b, ah);
        }
        public void ShowResult(int? a, int? b, OpearteEventHandler handler)
        {
            string result = "";
            result += handler(a, b).ToString();
            Console.WriteLine(result);
            Console.ReadKey();
        }
        public int? Add(int? a, int? b)
        {
            return a + b;
        }
        public int? Sub(int? a, int? b)
        {
            return a - b;
        }
        public int? Multiply(int? a, int? b)
        {
            return a * b;
        }

    }
}

對上面的代碼進行調試發現,Add方法和Sub方法的結果值並沒有被返回,只返回了最后Multiply的值,除了上面這個原因外,發布者和訂閱者的關系是松耦合的,發布者根本不關心誰訂閱了它的事件,為什么要訂閱,跟別說返回值了,發布者要做的就是執行訂閱它事件的方法,所以當委托綁定了多個事件時,返回值常常是void的原因.

 

2、如何讓事件只允許一個客戶訂閱

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

namespace Delegate
{
    public delegate string GeneralEventHandler(); 
    class Program
    {
        static void Main(string[] args)
        {
            Publisher pr = new Publisher();
            pr.Register(new Program().ReturnValue);
            pr.DoSomething();
        }
        public string ReturnValue() 
        {
            return "返回值";
        }
        public string ReturnValue1()
        {
            return "返回值1";
        }
        public class Publisher
        {
            private event GeneralEventHandler NumberChanged;//聲明一個事件
            public void Register(GeneralEventHandler handler)
            {
                NumberChanged += handler;
            }
            public void UnRegister(GeneralEventHandler handler)
            {
                NumberChanged -= handler;
            }
            public void DoSomething()
            {
                if (NumberChanged != null) 
                {
                    string str = NumberChanged();
                    Console.WriteLine("return value is {0}", str);
                }
            }
        }
    }
}

注意:

(1)、在UnRegister()中,沒有進行任何判斷就使用了NumberChanged -= method 語句。這是因為即使method 方法沒有進行過注冊,此行語句也不會有任何問題,不會拋出異常,僅僅是不會產生任何效果而已。

(2)、NumberChanged被聲明為私有的,所以客戶端無法看到它,所以無法通過它來觸發事件,調用訂閱者的方法,而只能通過Register()和UnRegister()方法來注冊和取消注冊

但是上面的代碼並不是最好的實現,C#提供事件訪問器,也可以實現上面的功能

 

3、事件訪問器

C#提供事件訪問器,通過它可以將委托封裝成一個變量,像訪問類中的屬性那樣,來訪問事件,代碼如下:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Delegate
{
    public delegate string GeneralEventHandler(); 
    class Program
    {
        static void Main(string[] args)
        {
            Publisher pr = new Publisher();
            pr.NumberChanged += new Subscriber().ReturnValue;
            pr.DoSomethings();
        }
        public class Publisher
        {
            private GeneralEventHandler numberChanged;//聲明一個委托變量
            // 事件訪問器的定義 
            public event GeneralEventHandler NumberChanged 
            {
                add 
                {
                    numberChanged = value;
                }
                remove
                {
                    numberChanged -= value;
                }
            }
            public void DoSomethings()
            {
                if (numberChanged != null) 
                {
                    string str = numberChanged();
                    Console.WriteLine("return value is {0}", str);
                }
            }
        }
        public class Subscriber
        {
            public string ReturnValue() 
            {
                return "返回值1";
            }
        }
    }
}

上面代碼中的類似屬性的public event GeneralEventHandler NumberChanged{ add{....}remove{....} }就是事件訪問器了,使用了事件訪問器之后,DoSomethings就只能通過numberChanged委托變量來觸發事件了,而不能使用NumberChanged訪問器來觸發,因為它只用於注冊和取消注冊事件。

 

4、獲得多個返回值與異常處理

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

namespace Delegates
{
    public delegate string GeneralEventHandler(int num); 
    class Program
    {
        static void Main(string[] args)
        {
            Publisher pr = new Publisher();
            pr.NumberChanged +=new GeneralEventHandler(new Subscriber().ReturnValue);
            pr.NumberChanged += new GeneralEventHandler(new Subscriber1().ReturnValue);
             List<string> list=pr.DoSomething();
             Console.WriteLine(list[0]+"..."+list[1]);
        }
        public class Publisher
        {
            public event GeneralEventHandler NumberChanged;//定義一個事件
            public List<string> DoSomething()
            {
                List<string> list = new List<string>();
                if (NumberChanged == null) return list;
                //獲得委托數組
                Delegate[] delArr = NumberChanged.GetInvocationList();
                foreach (Delegate del in delArr) {
                    GeneralEventHandler handler = (GeneralEventHandler)del;//拆箱
                    list.Add(handler(100));
                }
                return list;
            }
          
        }

        public class Subscriber
        {
            public string ReturnValue(int num) 
            {
                Console.WriteLine("Subscriber1 invoked, number:{0}", num);
                return "[Subscriber returned]";
            }
        }
        public class Subscriber1
        {
            public string ReturnValue(int num)
            {
                Console.WriteLine("Subscriber1 invoked, number:{0}", num);
                return "[Subscriber1 returned]";
            }
        }
    }
}

上面的方法獲得了兩個訂閱者的返回值,但是前面說過很多情況下,委托的定義都不包含返回值,所以上面的方法介紹的似乎沒什么實際意義。但是其實上面這種方法來觸發事件的情況應該是在異常處理中,因為很有可能在觸發事件時,訂閱者的方法拋出異常,這一異常可能會引起發布者的異常,使得發布者的程序停止,而后面的訂閱者的方法將不會被執行,所以我們需要加上異常處理。

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

namespace Delegates
{
    public delegate void GeneralEventHandler(object sender, EventArgs e); 
    class Program
    {
        static void Main(string[] args)
        {
            Publisher pr = new Publisher();
            pr.NumberChanged += new GeneralEventHandler(new Subscriber().OnEvent);
            pr.NumberChanged += new GeneralEventHandler(new Subscriber1().OnEvent);
            pr.NumberChanged += new GeneralEventHandler(new Subscriber2().OnEvent);
            pr.DoSomethings();

        }
        public class Publisher
        {
            public event GeneralEventHandler NumberChanged;//定義一個事件
            public void DoSomethings() {
                Program.TraverseEvent(NumberChanged, this, EventArgs.Empty);
            }
          
        }

        public class Subscriber
        {
            public void OnEvent(object sender, EventArgs e) { Console.WriteLine("Subscriber Invoked!"); }
        }
        public class Subscriber1
        {
            public void OnEvent(object sender, EventArgs e) { throw new Exception("Subscriber1 Failed"); } 
        }
        public class Subscriber2
        {
            public void OnEvent(object sender, EventArgs e) {  Console.WriteLine("Subscriber2 Invoked!"); }
        }

        /// <summary>
        /// 遍歷所有的訂閱事件,獲得多個返回值以及異常處理
        /// </summary>
        /// <param name="del">綁定完方法的委托</param>
        /// <param name="args">傳遞給訂閱方法的參數</param>
        /// <returns></returns>
        public static object[] TraverseEvent(Delegate del, params object[] args)
        {
            List<object> list = new List<object>();
            if (del != null)
            {
                Delegate[] delArr = del.GetInvocationList();//獲得委托鏈表
                foreach (Delegate method in delArr)
                {
                    try
                    {
                        object obj = method.DynamicInvoke(args);//執行訂閱方法,並傳遞參數,獲得其返回值
                        if (obj != null)
                        {
                            list.Add(obj);
                        }
                    }
                    catch{
                    }
                }
            }
            return list.ToArray();
        }
    }
}

DynamicInvoke方法是調用委托最通用的方法了,適用於所有類型的委托。它接受的參數為object[],也就是說它可以將任意數量的任意類型作為參數,並返回單個object 對象。

ok,通過結果發現Subscriber1().OnEvent訂閱方法拋出的異常並沒有影響Subscriber2().OnEvent的方法的執行。當然因為

catch什么都沒有做!

 

5、訂閱者方法超時的處理

訂閱者除了可以通過異常的方式影響發布者外,還可以通過另外一種方式影響發布者:超時,一般說超時指的是方法的執行超過了某個時間,而這里的含義是,方法的執行的時間比較長,2s、3s、5s都算做超時,是一個很模糊的概念。而超時和異常的區別就在於,超時並不會影響事件的正確觸發和正常的運行,卻會導致事件觸發后需要很長時間才會結束,在依次執行訂閱者方法的這段時間內,客戶端程序會被中斷,什么也不能做。應為當執行訂閱者中的方法時(通過委托相當於依次調用了所有注冊了的方法),當前線程會轉到訂閱者的方法中,調用訂閱者方法的客戶端則會被中斷,只有當方法執行完畢並返回時,控制權才會重新回到調用訂閱者方法的客戶端的客戶端中。如果你調試過上面案例的代碼的話,我相信這個特點不難發現。

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

namespace Delegates
{

    public class Program
    {

        static void Main(string[] args)
        {
            Publisher pr = new Publisher();
            pr.MyEvent += new EventHandler(new Subscriber().OnEvent);
            pr.MyEvent += new EventHandler(new Subscriber1().OnEvent);
            pr.MyEvent += new EventHandler(new Subscriber2().OnEvent);
            pr.DoSomethings();
        }

        public static object[] traverseEvent(Delegate del,params object[] args) {
            List<object> list = new List<object>();
            if (del != null) {
                Delegate[] dels=del.GetInvocationList();
                foreach (Delegate method in dels) {
                    try
                    {
                        //執行傳入委托的訂閱方法,傳入參數,並且獲得其返回值,而且如果傳入委托中間部分的訂閱事件發生異常,不會影響后面事件的執行
                        object obj = method.DynamicInvoke(args);
                        if (obj != null)
                        {
                            list.Add(obj);
                        }
                    }
                    catch{
                        
                    }
                }
            }
            return list.ToArray();
        }
    }
    public class Publisher
    {
        public event EventHandler MyEvent;
        public void DoSomethings() {
            Console.WriteLine("DoSomethings invoked!");
            Program.traverseEvent(MyEvent, this, EventArgs.Empty);
        }
    }

    public class Subscriber 
    {
        public void OnEvent(object sender,EventArgs args) {
            throw new Exception("Subscriber Failed");
        }
    }

    public class Subscriber1
    {
        public void OnEvent(object sender, EventArgs args)
        {
            Console.WriteLine("Subscriber1 begin execute,time is {0}", DateTime.Now.ToLongTimeString());
            Thread.Sleep(3000);
            Console.WriteLine("Wait for 3 seconds,Subscriber1 executed end, time is {0}", DateTime.Now.ToLongTimeString());
        }
    }
    public class Subscriber2
    {
        public void OnEvent(object sender, EventArgs args)
        {
            Console.WriteLine("Subscriber2 begin execute,time is {0}",DateTime.Now.ToLongTimeString());
            Console.ReadKey();
        }
    }
}

通過方法的執行事件可以發現,Subscriber2方法是在Subscriber1的方法等待3秒之后才執行的,但是在前面說過,很多情況下,尤其是在遠程調用的時候(比如所在Remoting中),發布者和訂閱者應該是完全的松耦合的,發布者不關心誰訂閱了它,為什么要訂閱它,訂閱它的方法有什么返回值,不關心訂閱者方法會不會拋出異常,當然也不關心訂閱者方法需要多少時間才能執行完畢.它只要在事件的發生的一剎那告訴訂閱者事件已經發生,並將相關參數傳遞給訂閱者事件。而訂閱者方法不管是執行失敗還是超時都不應該影響發布者,而上面的例子發布者必須等待Subscriber1中的發發執行完畢才會執行Subscriber2中的方法,所以需要解決這個問題。

 

我們都知道委托實際上是一種數據結構,當每定義一個委托,實際上這個委托實例都會繼承自MulticastDelegate這個完整的類,而MulticastDelegate這個類則會繼承Delegate數據結構,而MulticastDelegate類中包含Invoke()和BeginInvoke()和EndInvoke()等方法,所以間接的每個委托的實例也可以調用這些方法。下面是一個委托被調用的過程:
(1)、調用Invoke方法,中斷發布者客戶端的操作

(2)、開啟一個線程

(3)、通過線程去執行所有訂閱者的方法

(4)、所有訂閱者方法執行完畢,將控制權返還給發布者客戶端

注意:Invoke()方法是同步執行的,也就是說如果某一個訂閱方法超時了,那么其下面的方法會等到它執行完畢之后,在執行

ok,介紹完Invoke之后,想必上面的超時問題為什么會發生,應該一目了然了,結下了開始講解決方法,BeginInvoke()和EndInvoke()方法,在.NET中異步執行的方法通常會成對出現,並且以Begin和End作為方法的開頭(如Stream 類的BeginRead()和EndRead()方法了),他們用於方法的異步執行.

(1)、BeginInvoke()方法簡介:即在發布者客戶端吊用委托之后,當前委托實例調用BeginInvoke()方法,該方法是異步執行,它會從線程池中抓取一個閑置線程,交由這個線程去執行訂閱者中的方法,而客戶端線程則繼續執行接下來的代碼,通過這種多線程的方式,達到了異步的效果,也避免了上面單線程阻塞的問題。

(2)、BeginInvoke()方法接受"動態"的參數個數和類型,具體的參數個數是根據調用BeginInvoke方法的委托所決定的,代碼如下:

public delegate void  EventHandler1(string a,int b);
eh.BeginInvoke("a", 1, null, null);

這里的代碼可能不合理,但只是舉例說明,這里調用BeginInvoke()方法的是EventHandler,EventHandler委托接受兩個參數string和int,所以BeginInvoke前兩個參數也是string和int,這個是編譯時,根據委托的定義動態生成的.

(3)、BeginInvoke()方法接受"動態"的參數個數和類型,但最后兩個參數是確定的,一個是AsyncCallback(回調函數),另一個是object

(4)、當在委托上調用BeginInvoke方法時,當委托對象只能包含一個方法,對於有多個訂閱者注冊的情況,只能通過GetInvocationList()獲取委托鏈表,遍歷它們,分別操作

(5)、如果訂閱者方法拋出異常,.NET會捕捉到它,但是只有在調用EndInvoke()方法時,才會將異常拋出,在本例中,因為我們不關心訂閱者的情況,所以無需處理異常,因為即使異常拋出,也是在執行訂閱者方法的線程上,所以不會影響到發布者客戶端,客戶端甚至不知道訂閱者發生了異常,這有時是好事有時是壞事.

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

namespace Delegates
{
    public delegate void GeneralEventHandler(object sender, EventArgs e); 
    class Program
    {
        static void Main(string[] args)
        {

            Publisher pr = new Publisher();
            pr.NumberChanged += new EventHandler(new Subscriber().OnEvent);
            pr.NumberChanged += new EventHandler(new Subscriber1().OnEvent);
            pr.NumberChanged += new EventHandler(new Subscriber2().OnEvent);
            pr.DoSomethings();

        }
        public class Publisher
        {
            public event EventHandler NumberChanged;//定義一個事件
            public void DoSomethings() {
                if (NumberChanged != null) {
                    Delegate[] delArr = NumberChanged.GetInvocationList();//獲得委托鏈表
                    foreach (Delegate method in delArr) {
                        EventHandler handler = (EventHandler)method;
                        handler.BeginInvoke(this, EventArgs.Empty, null, null);
                    }
                }
            }
          
        }

        public class Subscriber
        {
            public void OnEvent(object sender, EventArgs e) {
                Thread.Sleep(TimeSpan.FromSeconds(3));//模擬休息3秒
                Console.WriteLine("Wait for 3 seconds,Subscriber Invoked!"); 
            }
        }
        public class Subscriber1
        {
            public void OnEvent(object sender, EventArgs e) { 
                throw new Exception("Subscriber1 Failed"); //模擬拋出異常
            } 
        }
        public class Subscriber2
        {
            public void OnEvent(object sender, EventArgs e) {  Console.WriteLine("Subscriber2 Invoked!"); }
        }

    }
}

ok,通過結果發現,Subscriber2的方法最先執行,並沒有等待Subscriber的方法執行完畢,而且Subscriber1的異常也沒有拋出,發布者客戶端並沒有因為這個異常而停止操作。

 

 6、委托和方法的異步調用

通常情況下,如果需要異步執行一個耗時的操作,我們會新開一個線程,然后讓這個線程去執行代碼。但是對於每一個異步調用都用線程去操作顯然會對性能造成影響,同時操作也相對繁瑣一些,.NET中可以通過委托進行方法的異步調用,就是說客戶端在異步調用方法時,本身並不會因為方法的調用而終止,而是從線程中抓取一個線程去執行該方法,主線程繼續執行自己的代碼,這樣就實現了代碼的並行執行,使用線程池的好處就是避免了頻繁的進行異步調用時,創建、銷毀線程的開銷。

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

namespace Delegates
{
    public delegate int AddEventHandler(int a,int b);
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Client Application Started!The time is {0}",DateTime.Now);
            Thread.CurrentThread.Name = "主線程";
            Calculator cal = new Calculator();
            AddEventHandler handler = new AddEventHandler(cal.Add);
            IAsyncResult asyncResult = handler.BeginInvoke(6, 6, null, null);
            for (int i = 1; i <= 3; i++)
            {
                Thread.Sleep(TimeSpan.FromSeconds(1));
                Console.WriteLine("{0}: 主線程休息了 {1} 秒.", Thread.CurrentThread.Name, 1);
            }
            int res = handler.EndInvoke(asyncResult);
            Console.WriteLine("異步調用Add方法的結果: {0}\n", res);
            Console.WriteLine("Client Application ended!The time is {0}", DateTime.Now);
            Console.WriteLine("按任意鍵繼續..."); 

            Console.ReadLine();
        }
    }
}
public class Calculator
{
    public int Add(int a, int b)
    {
        if (Thread.CurrentThread.IsThreadPoolThread)
        {
            Thread.CurrentThread.Name = "線程池中的線程";
        }
        Console.WriteLine("-----------------------------------------------");
        Console.WriteLine("Add方法開始執行!");
        for (int i = 0; i <= 2; i++)
        {
            Thread.Sleep(TimeSpan.FromSeconds(1));
            Console.WriteLine("{0}: 休息了 {1} 秒.", Thread.CurrentThread.Name, 1);
        }
        Console.WriteLine("Add方法執行完畢!");
        Console.WriteLine("-----------------------------------------------");
        return a + b;
    }

}

從輸出可以看出,整個應用程序執行了3秒種時間,但是主線程和子線程一共休息了6秒,所以可以推斷出,主線程和子線程是並行的,不是串行的EndInvoke方法獲得了返回值.

 

接下來說BeginInvoke方法的另外兩個參數,一個是AsyncCallback是一個委托類型,它用於方法的回調,也就是當異步方法調用完畢時,自動調用的方法,它的定義為:

public delegate void AsyncCallback(IAsyncResult ar);

第二個參數是Object類型用於傳遞任何你想要的數據,它可以通過IAsyncResult的AsyncState屬性獲得

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Remoting.Messaging;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace Delegates
{
    public delegate int AddEventHandler(int a,int b);
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Client Application Started!The time is {0}",DateTime.Now);
            Thread.CurrentThread.Name = "主線程";
            Calculator cal = new Calculator();
            AddEventHandler handler = new AddEventHandler(cal.Add);
            AsyncCallback callBack = new AsyncCallback(OnAddComplete);
            int data = 666;
            IAsyncResult asyncResult = handler.BeginInvoke(6, 6, callBack, data);
            for (int i = 1; i <= 3; i++)
            {
                Thread.Sleep(TimeSpan.FromSeconds(1));
                Console.WriteLine("{0}: 主線程休息了 {1} 秒.", Thread.CurrentThread.Name, 1);
            }
            Console.WriteLine("Client Application ended!The time is {0}", DateTime.Now);
            Console.WriteLine("按任意鍵繼續..."); 

            Console.ReadLine();
        }
        static void OnAddComplete(IAsyncResult asyncResult) 
        { 
            AsyncResult result = (AsyncResult)asyncResult;
            AddEventHandler del = (AddEventHandler)result.AsyncDelegate;
            int data = (int)asyncResult.AsyncState; 
            int rtn = del.EndInvoke(asyncResult);
            Console.WriteLine("{0}: Result, {1}; Data: {2}\n", Thread.CurrentThread.Name, rtn, data); 
        } 
    }
}
public class Calculator
{
    public int Add(int a, int b)
    {
        if (Thread.CurrentThread.IsThreadPoolThread)
        {
            Thread.CurrentThread.Name = "線程池中的線程";
        }
        Console.WriteLine("-----------------------------------------------");
        Console.WriteLine("Add方法開始執行!");
        for (int i = 0; i <= 2; i++)
        {
            Thread.Sleep(TimeSpan.FromSeconds(1));
            Console.WriteLine("{0}: 休息了 {1} 秒.", Thread.CurrentThread.Name, 1);
        }
        Console.WriteLine("Add方法執行完畢!");
        Console.WriteLine("-----------------------------------------------");
        return a + b;
    }

}

ok,異步方法執行完畢之后,立即調用了OnAddComplete方法,並且data數據成功傳遞了過去;

注意:

(1)、在調用EndInvoke方法時可能會拋出異常,所以需要加到try{}catch{}塊中

(2)、執行回調方法的線程並不是Main Thread,而是Pool Thread

(3)、我們在調用BeginInvoke()后不再需要保存IAysncResult 了,因為AysncCallback 委托將該對象定義在了回調方法的參數列表中

(4)、通過BeginInvoke()最后一個Object參數,可以給回調函數傳參


 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM