委托进阶

委托进阶

 

前言

 

尽量每天都写点东西,就算是没有一点有价值的东西,也想写点关于自己的东西.

我的博客一般都是半日记的形式出现的.将来等我老了,说不定,我会感谢互联网带给我的这些回忆,我现在就很感谢互联网给我了很多东西.接下来谈定正事.

 

前面说了不少基础知识,大家有没有想过一个问题,为啥委托定义的返回值都是void?

尽管这是非必须的,但是大家会发现很多的委托定义返回值都为void,为啥呢?这是因为委托变量可以供多个订阅者注册,如果定义了返回值,那么多个订阅者的方法都会想发布者返回数值,结果就是后面一个范湖的方法值将前面的返回值覆盖掉了,因此,实际上只能获得最后一个方法调用的返回值.可以运行下面的代码测试一下.除了这个以外,由于发布者和订阅者是松耦合的,发布者根本不关心过谁订阅了它的事件,为什么要订阅,也更不用说订阅者的返回值了,所以返回订阅者的方法返回值在大多数情况下根本没有必要.

 

这里有个案例大家感兴趣的话,可以看看:

namespace 测试用例
{
    class Program
    {
        static void Main(string[] args)
        {
            Publishser pub = new Publishser();
            Subscriber1 sub1 = new Subscriber1();
            Subscriber2 sub2 = new Subscriber2();
            Subscriber3 sub3 = new Subscriber3();
 
            pub.NumberChanged += new GeneralEventHandler(sub1.OnNumberChanged);
            pub.NumberChanged += new GeneralEventHandler(sub2.OnNumberChanged);
            pub.NumberChanged += new GeneralEventHandler(sub3.OnNumberChanged);
            pub.DoSomething();
        }
    }
    //定义委托
    public delegate string GeneralEventHandler();
 
    //定义时间发布者
    public class Publishser
    {
        public event GeneralEventHandler NumberChanged;//声明一个事件
 
        public void DoSomething()
        {
            if (NumberChanged != null)//触发事件
            {
                string rtn = NumberChanged();
                Console.WriteLine(rtn);//打印返回的字符串,输出位Subscriber3
            }
        }
    }
 
    //定义事件订阅者
    public class Subscriber1
    {
        public string OnNumberChanged()
        {
            return "Subscriber1";
        }
    }
    public class Subscriber2
    {
        public string OnNumberChanged()
        {
            return "Subscriber2";
        }
    }
    public class Subscriber3
    {
        public string OnNumberChanged()
        {
            return "Subscriber3";
        }
    }
}


运行这段代码,应该能看到输出的只有Subscriber3,这样的话,只得到了最后一个注册方法的返回值.

 

如何让事件只允许一个客户订阅

在少数情况下,比如像前面,为了避免”值覆盖”的情况(更多是在异步调用方法时),可能想要限制只允许一个客户端注册.此时咋办呢?我们可以像下面这样,将事件声明为private,然后提供两个方法来进行注册和取消注册.

    //定义事件发布者
    public class Publishser
    {
        private event GeneralEventHandler NumberChanged;//声明一个私有事件
        //注册实践
        public void Register(GeneralEventHandler method)
        {
            NumberChanged = method;
        }
        //取消注册
        public void UnRegister(GeneralEventHandler method)
        {
            NumberChanged -= method;
        }
        public void DoDomething()
        {
            //做某些其余的事情
            if (NumberChanged != null)//触发事件
            {
                string rtn = NumberChanged();
                Console.WriteLine("Return: {0}", rtn);//打印返回的字符串,输出为Subscriber3
            }
        }
    }


注意上面,UnRegister(),没有进行任何判断就是用了NumberChanged-=method语句.这是因为及时method方法没有进行过注册,此行语句也不会有任何问题,不会抛出异常,仅仅是不会产生任何效果而已.

 

注意在Register()方法中,是用了赋值操作符”=”,而非”+=”,通过这种方式就避免了多个方法注册.上面的代码尽管可以满足我们的需求,但是此时大家还应该注意以下两点:

1.将NumberChanged声明为委托变量还是事件都无所谓,因为他是私有的,即便将它声明为一个委托变量,客户端也看不到它,也就无法通过它来触发事件,调用订阅者的方法.而只能通过Register()UnRegister()方法来进行注册和取消注册,通过调用DoSomething()方法触发事件(而不是NumberChanged本身).

2.还发现,这里采用的对NumberChanged委托变量的访问模式和C#中的属性十分类似.C#中设计类型时,一个属性通常会对应内部字段成员,在类型的外部对成员的操作全部通过属性来完成.尽管这里对委托变量的处理是类似的效果,却使用了两个方法进行模拟,有没有办法像使用属性一样来完成上面的例子呢?!C#中提供了一种叫事件访问器(Event Accessor)的东西,它用来封装委托变量.

如下例所示:

 

namespace 测试用例
{
    class Program
    {
        static void Main(string[] args)
        {
            Publishser pub = new Publishser();
            
            Subscriber1 sub1 = new Subscriber1();
            Subscriber2 sub2 = new Subscriber2();
 
 
            pub.NumberChanged -= sub1.OnNumberChanged;
            pub.NumberChanged += sub2.OnNumberChanged;
            pub.NumberChanged += sub1.OnNumberChanged;
            pub.DoDomething();
        }
    }
    //定义委托
    public delegate string GeneralEventHandler();
 
    //定义事件发布者
    public class Publishser
    {
        private  GeneralEventHandler numberChanged;//委托变量
        //事件访问器的定义
        public event GeneralEventHandler NumberChanged
        {
            add
            {
                numberChanged = value;
            }
            remove
            {
                numberChanged -= value;
 
            }
        }
        public void DoDomething()
        {
            //做某些其余的事情
            if (numberChanged != null)//通过委托变脸触发事件
            {
                string rtn = numberChanged();
                Console.WriteLine("Return: {0}", rtn);//打印返回的字符串,输出为Subscriber3
            }
        }
    }
 
    //定义事件订阅者
    public class Subscriber1
    {
        public string OnNumberChanged()
        {
            return "Subscriber1";
        }
    }
    public class Subscriber2
    {
        public string OnNumberChanged()
        {
            return "Subscriber2";
        }
    }
    public class Subscriber3
    {
        public string OnNumberChanged()
        {
            return "Subscriber3";
        }
    }
}

上述代码中,类似属性的public event GeneralEventHandler NumberChanged{add{...}remove{...}}语句便是事件访问器.在使用了事件访问器以后,DoSomething方法中便只能通过numberChanged委托变量来触发事件,而不能通过NumberChanged事件访问器(注意他们的大小写不同)触发,它只用于注册和取消注册.

 

获得多个返回值与异常处理

 

1.如何接受多个订阅者的返回值?

现在假设我们想要获得多个订阅者的返回值,List<string>的形式返回,那么该咋办呢?这里有一点需要留意,委托在编译后会生成一个继承自MulticastDelegate的类,而这个MulticastDelegate又继承自Delegate.Delegate内部,维护了一个委托链表,链表上的每一个元素为一个只包含一个目标方法的委托对象.而通过Delegate基类的GetInvocationList()静态方法,可以获得这个委托链表.随后遍历这个链表,通过链表中的每个委托对象来调用方法,这样就可以分别获得每个方法的返回值,说了这么多估计晕了...下面是案例:

 

namespace 测试用例
{
    class Program
    {
        static void Main(string[] args)
        {
            Publishser pub = new Publishser();
            
            Subscriber1 sub1 = new Subscriber1();
            Subscriber2 sub2 = new Subscriber2();
            Subscriber3 sub3 = new Subscriber3();
 
            pub.NumberChanged += new GeneralEventHandler(sub1.OnNumberChanged);            
            pub.NumberChanged += new GeneralEventHandler(sub2.OnNumberChanged);
            pub.NumberChanged += new GeneralEventHandler(sub3.OnNumberChanged);
 
            List<string> list = pub.DoSomething();
 
            foreach (string str  in list)
            {
                Console.WriteLine(str);
            }
            
        }
    }
    //定义委托
    public delegate string GeneralEventHandler(int num);
 
    //定义事件发布者
    public class Publishser
    {
        public event  GeneralEventHandler NumberChanged;//声明一个事件
 
        public List<string> DoSomething()
        {
            //做某些其他的事
            List<string> strList = new List<string>();
            if (NumberChanged==null)
            {
                return strList;
            }
            //获得委托数组
            Delegate[] delArray = NumberChanged.GetInvocationList();
 
            foreach (Delegate del in delArray)
            {
                //进行一个向下转换
                GeneralEventHandler method = (GeneralEventHandler)del;
                strList.Add(method(100));//调用方法并获得返回值
 
            }
            return strList;
        }
    }
 
    //定义事件订阅者
    public class Subscriber1
    {
        public string OnNumberChanged(int num)
        {
            Console.WriteLine("Subscriber1 invoked,number:{0}",num);
            return "[Subscriber1 returned]";
        }
    }
    public class Subscriber2
    {
        public string OnNumberChanged(int num)
        {
            Console.WriteLine("Subscriber2 invoked,number:{0}", num);
            return "[Subscriber2 returned]";
        }
    }
    public class Subscriber3
    {
        public string OnNumberChanged(int num)
        {
            Console.WriteLine("Subscriber3 invoked,number:{0}", num);
            return "[Subscriber3 returned]";
        }
    }
}

运行上面的代码,可以看看输出.

 

2.异常处理

可以看到获得了三个方法的返回值.前面说过,在很多情况下委托的定义都不包含返回值,所以上面介绍的方法似乎没有什么实际意义.其实通过这种方式来触发事件最常见的情况应该是在异常处理中,因为很有可能在触发事件时,订阅者的方法会抛出异常,而这一异常会直接影响到发布者,使发布者程序终止,而后面订阅者的方法将不会被执行.因此我们需要加上异常处理:

namespace 测试用例
{
    class Program
    {
        static void Main(string[] args)
        {
            Publishser pub = new Publishser();
            
            Subscriber1 sub1 = new Subscriber1();
            Subscriber2 sub2 = new Subscriber2();
            Subscriber3 sub3 = new Subscriber3();
            pub.MyEvent += new EventHandler(sub1.OnEvent);
            pub.MyEvent += new EventHandler(sub2.OnEvent);
            pub.MyEvent += new EventHandler(sub3.OnEvent);
            
 
            pub.DoSomething();
        }
    }
 
 
    //定义事件发布者
    public class Publishser
    {
        public event EventHandler MyEvent;
        public void DoSomething()
        {
            //做些事情
            if (MyEvent!=null)
            {
                try
                {
                    MyEvent(this, EventArgs.Empty);
                }
                catch (Exception e)
                {
                    Console.WriteLine("Exception: {0}",e.Message);
                    
                }
            }
        }
    }
 
    //定义事件订阅者
    public class Subscriber1
    {
        public void OnEvent(object sender,EventArgs e)
        {
            Console.WriteLine("Subscriber1 Invoked!");
        }
    }
    public class Subscriber2
    {
        public void OnEvent(object sender, EventArgs e)
        {
            throw new Exception("Subscriber2 Failed!");            
        }
    }
    public class Subscriber3
    {
        public void OnEvent(object sender, EventArgs e)
        {
            Console.WriteLine("Subscriber3 Invoked!");
        }
    }
}
 



注意到在Subscriber2中抛出了异常,同时在Publisher中使用了try/catch语句来处理异常.运行上面的代码,我们得到一组输出:

Subscriber1 Invoked!
Exception: Subscriber2 Failed!


可以看到,尽管我们不活了异常,是程序没有异常中断,但是却影响了后面的订阅者,因为Subscriber3也订阅了事件,但是却没有受到事件通知(他的方法没有被调用).此时,可以采用前面的办法,先获得委托链表,然后在遍历链表的循环中处理异常,只需要修改一下DoSomething方法就可以了:

 

        public void DoSomething()
        {
            //做些事情
            if (MyEvent!=null)
            {
                Delegate[] delArray = MyEvent.GetInvocationList();
                foreach (Delegate del in delArray)
                {
                    EventHandler method = (EventHandler)del;//强制转换为具体的委托类型
                    try
                    {
                        MyEvent(this, EventArgs.Empty);
                    }
                    catch (Exception e)
                    {
                        Console.WriteLine("Exception: {0}", e.Message);
                    }
                }
                
            }
        }

注意,DelegateEventHandler的基类,所以为了触发事件,首先要进行一个向下的强制转换,之后才能在其上触发事件,调用所有注册对象的方法.除了使用这种方式以外,还有一种更灵活的方式可以调用方法,他是定义在Delegate基类中的DynamicInvoke()方法:

public object DynamicInvoke(params object[] args);


 

这可能是调用委托最通用的方法了,适用于所有类型的委托.接受的参数为object[],也就是说它可以将任意数量的任意类型作为参数,并返回单个object对象.上面的DoSomething()方法也可以改写成下面这种通用形式:

        public void DoSomething()
        {
            //做些事情
            if (MyEvent!=null)
            {
                Delegate[] delArray = MyEvent.GetInvocationList();
                foreach (Delegate del in delArray)
                {
                    
                    try
                    {
                        //使用DynamicInvoke方法触发事件
                        del.DynamicInvoke(this,EventArgs.Empty);
                    }
                    catch (Exception e)
                    {
                        Console.WriteLine("Exception: {0}", e.Message);
                    }
                }
                
            }
        }
 


注意,目前在DoSomething()方法中,已经取消了向具体委托类型的向下转型,现在没有了任何基于特定委托的代码,DynamicInvoke又可以接受任何类型的参数,且返回了一个object对象.所以完全可以将DOsomething()方法抽象出来,使它成为一个公共方法,然后供其他类来调用.我们将这个方法声明为静态的,然后定义在Program类中:

 

    class Program
    {
        static void Main(string[] args)
        {
            Publishser pub = new Publishser();
            
            Subscriber1 sub1 = new Subscriber1();
            Subscriber2 sub2 = new Subscriber2();
            Subscriber3 sub3 = new Subscriber3();
            pub.MyEvent += new EventHandler(sub1.OnEvent);
            pub.MyEvent += new EventHandler(sub2.OnEvent);
            pub.MyEvent += new EventHandler(sub3.OnEvent);
            
 
            pub.DoSomething();
        }
 
        //触发某个事件,以列表的形式返回所有方法的返回值
        public static object[] FireEvent(Delegate del, params object[] args)
        {
            List<object> objList = new List<object>();
            if (del!=null)
            {
                Delegate[] delArray = del.GetInvocationList();
                foreach (Delegate method in delArray)
                {
                    try
                    {
                        //使用DynamicInvoke方法触发事件
                        object obj = method.DynamicInvoke(args);
                        if (obj!=null)
                        {
                            objList.Add(obj);
                        }
                    }
                    catch (Exception)
                    {                        
                    }
                }
            }
            return objList.ToArray();
        }       
    }
 

随后,DoSomething()中只要简单的调用一下这个方法就可以了:

    public class Publishser
    {
        public event EventHandler MyEvent;
        public void DoSomething()
        {
            //做些事情
            Program.FireEvent(MyEvent,this,EventArgs.Empty);                            
        }
    }


注意,FireEvent()方法还可以返回一个object[]数组,这个数组包括了所有订阅者方法的返回值.

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值