委托进阶
前言
尽量每天都写点东西,就算是没有一点有价值的东西,也想写点关于自己的东西.
我的博客一般都是半日记的形式出现的.将来等我老了,说不定,我会感谢互联网带给我的这些回忆,我现在就很感谢互联网给我了很多东西.接下来谈定正事.
前面说了不少基础知识,大家有没有想过一个问题,为啥委托定义的返回值都是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);
}
}
}
}
注意,Delegate是EventHandler的基类,所以为了触发事件,首先要进行一个向下的强制转换,之后才能在其上触发事件,调用所有注册对象的方法.除了使用这种方式以外,还有一种更灵活的方式可以调用方法,他是定义在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[]数组,这个数组包括了所有订阅者方法的返回值.