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参数,可以给回调函数传参


 

 

posted @ 2017-05-16 17:30  郑小超  阅读(10166)  评论(0编辑  收藏  举报