当前位置: 移动技术网 > IT编程>开发语言>.net > C#中的委托和事件学习(续)

C#中的委托和事件学习(续)

2018年04月21日  | 移动技术网IT编程  | 我要评论

gif动态图长 污连续的,适合婚礼上唱的歌,终极面试剧情


如何让事件只允许一个客户订阅?
少数情况下,比如像上面,为了避免发生“值覆盖”的情况(更多是在异步调用方法时,后面会讨论),我们可能想限制只允许一个客户端注册。此时怎么做呢?我们可以向下面这样,将事件声明为private的,然后提供两个方法来进行注册和取消注册:

// 定义事件发布者
public class publishser {
private event generaleventhandler numberchanged; // 声明一个私有事件
// 注册事件
public void register(generaleventhandler method) {
numberchanged = method;
}
// 取消注册
public void unregister(generaleventhandler method) {
numberchanged -= method;
}

public void dosomething() {
// 做某些其余的事情
if (numberchanged != null) { // 触发事件
string rtn = numberchanged();
console.writeline("return: {0}", rtn); // 打印返回的字符串,输出为subscriber3
}
}
}

note:注意上面,在unregister()中,没有进行任何判断就使用了numberchanged-=method语句。这是因为即使method方法没有进行过注册,此行语句也不会有任何问题,不会抛出异常,仅仅是不会产生任何效果而已。

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

1、将numberchanged声明为委托变量还是事件都无所谓了,因为它是私有的,即便将它声明为一个委托变量,客户端也看不到它,也就无法通过它来触发事件、调用订阅者的方法。而只能通过register()和unregister()方法来注册和取消注册,通过调用dosomething()方法触发事件(而不是numberchanged本身,这在前面已经讨论过了)。

2、我们还应该发现,这里采用的、对numberchanged委托变量的访问模式和c#中的属性是多么类似啊?大家知道,在c#中通常一个属性对应一个类型成员,而在类型的外部对成员的操作全部通过属性来完成。尽管这里对委托变量的处理是类似的效果,但却使用了两个方法来进行模拟,有没有办法像使用属性一样来完成上面的例子呢?答案是有的,c#中提供了一种叫事件访问器(event accessor)的东西,它用来封装委托变量。如下面例子所示:

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; // 注册了sub2
pub.numberchanged += sub1.onnumberchanged; // sub1将sub2的覆盖掉了

pub.dosomething(); // 触发事件
}
}

// 定义委托
public delegate string generaleventhandler();

// 定义事件发布者
public class publishser {
// 声明一个委托变量
private generaleventhandler numberchanged;
// 事件访问器的定义
public event generaleventhandler numberchanged {
add {
numberchanged = value;
}
remove {
numberchanged -= value;
}
}

public void dosomething() {
// 做某些其他的事情
if (numberchanged != null) { // 通过委托变量触发事件
string rtn = numberchanged();
console.writeline("return: {0}", rtn); // 打印返回的字符串
}
}
}

// 定义事件订阅者
public class subscriber1 {
public string onnumberchanged() {
console.writeline("subscriber1 invoked!");
return "subscriber1";
}
}
public class subscriber2 {/* 与上类同,略 */}
public class subscriber3 {/* 与上类同,略 */}

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

subscriber1 invoked!
return: subscriber1

获得多个返回值与异常处理
现在假设我们想要获得多个订阅者的返回值,以list<string>的形式返回,该如何做呢?我们应该记得委托定义在编译时会生成一个继承自multicastdelegate的类,而这个multicastdelegate又继承自delegate,在delegate内部,维护了一个委托链表,链表上的每一个元素,为一个只包含一个目标方法的委托对象。而通过delegate基类的getinvocationlist()静态方法,可以获得这个委托链表。随后我们遍历这个链表,通过链表中的每个委托对象来调用方法,这样就可以分别获得每个方法的返回值:

class program4 {
static void main(string[] args) {
publishser pub = new publishser();
subscriber1 sub1 = new subscriber1();
subscriber2 sub2 = new subscriber2();
subscriber3 sub3 = new subscriber3();

pub.numberchanged += new demoeventhandler(sub1.onnumberchanged);
pub.numberchanged += new demoeventhandler(sub2.onnumberchanged);
pub.numberchanged += new demoeventhandler(sub3.onnumberchanged);

list<string> list = pub.dosomething(); //调用方法,在方法内触发事件

foreach (string str in list) {
console.writeline(str);
}
}
}

public delegate string demoeventhandler(int num);

// 定义事件发布者
public class publishser {
public event demoeventhandler 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) {
// 进行一个向下转换
demoeventhandler method = (demoeventhandler)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 subscriber3 {与上面类同,略}
public class subscriber3 {与上面类同,略}

如果运行上面的代码,可以得到这样的输出:

subscriber1 invoked, number:100
subscriber2 invoked, number:100
subscriber3 invoked, number:100
[subscriber1 returned]
[subscriber2 returned]
[subscriber3 returned]

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

class program5 {
static void main(string[] args) {
publisher pub = new publisher();
subscriber1 sub1 = new subscriber1();
subscriber2 sub2 = new subscriber2();
subscriber3 sub3 = new subscriber3();

pub.numberchanged += new demoeventhandler(sub1.onnumberchanged);
pub.numberchanged += new demoeventhandler(sub2.onnumberchanged);
pub.numberchanged += new demoeventhandler(sub3.onnumberchanged);
}
}

public class publisher {
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 {/* 与subsciber1类同,略*/}

注意到我们在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 {
method(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类中:

// 触发某个事件,以列表形式返回所有方法的返回值
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 { }
}
}
return objlist.toarray();
}

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

public void dosomething() {
// 做某些其他的事情
program5.fireevent(myevent, this, eventargs.empty);
}

注意fireevent()方法还可以返回一个object[]数组,这个数组包括了所有订阅者方法的返回值。而在上面的例子中,我没有演示如何获取并使用这个数组,为了节省篇幅,这里也不再赘述了,在本文附带的代码中,有关于这部分的演示,有兴趣的朋友可以下载下来看看。

委托中订阅者方法超时的处理
订阅者除了可以通过异常的方式来影响发布者以外,还可以通过另一种方式:超时。一般说超时,指的是方法的执行超过某个指定的时间,而这里我将含义扩展了一下,凡是方法执行的时间比较长,我就认为它超时了,这个“比较长”是一个比较模糊的概念,2秒、3秒、5秒都可以视为超时。超时和异常的区别就是超时并不会影响事件的正确触发和程序的正常运行,却会导致事件触发后需要很长才能够结束。在依次执行订阅者的方法这段期间内,客户端程序会被中断,什么也不能做。因为当执行订阅者方法时(通过委托,相当于依次调用所有注册了的方法),当前线程会转去执行方法中的代码,调用方法的客户端会被中断,只有当方法执行完毕并返回时,控制权才会回到客户端,从而继续执行下面的代码。我们来看一下下面一个例子:

class program6 {
static void main(string[] args) {

publisher pub = new publisher();
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(); // 触发事件

console.writeline(" control back to client!"); // 返回控制权
}

// 触发某个事件,以列表形式返回所有方法的返回值
public static object[] fireevent(delegate del, params object[] args) {
// 代码与上同,略
}
}

public class publisher {
public event eventhandler myevent;
public void dosomething() {
// 做某些其他的事情
console.writeline("dosomething invoked!");
program6.fireevent(myevent, this, eventargs.empty); //触发事件
}
}

public class subscriber1 {
public void onevent(object sender, eventargs e) {
thread.sleep(timespan.fromseconds(3));
console.writeline("waited for 3 seconds, subscriber1 invoked!");
}
}
public class subscriber2 {
public void onevent(object sender, eventargs e) {
console.writeline("subscriber2 immediately invoked!");
}
}
public class subscriber3 {
public void onevent(object sender, eventargs e) {
thread.sleep(timespan.fromseconds(2));
console.writeline("waited for 2 seconds, subscriber2 invoked!");
}
}

在这段代码中,我们使用thread.sleep()静态方法模拟了方法超时的情况。其中subscriber1.onevent()需要三秒钟完成,subscriber2.onevent()立即执行,subscriber3.onevent需要两秒完成。这段代码完全可以正常输出,也没有异常抛出(如果有,也仅仅是该订阅者被忽略掉),下面是输出的情况:

dosomething invoked!
waited for 3 seconds, subscriber1 invoked!
subscriber2 immediately invoked!
waited for 2 seconds, subscriber2 invoked!

control back to client!

但是这段程序在调用方法dosomething()、打印了“dosomething invoked”之后,触发了事件,随后必须等订阅者的三个方法全部执行完毕了之后,也就是大概5秒钟的时间,才能继续执行下面的语句,也就是打印“control back to client”。而我们前面说过,很多情况下,尤其是远程调用的时候(比如说在remoting中),发布者和订阅者应该是完全的松耦合,发布者不关心谁订阅了它、不关心订阅者的方法有什么返回值、不关心订阅者会不会抛出异常,当然也不关心订阅者需要多长时间才能完成订阅的方法,它只要在事件发生的那一瞬间告知订阅者事件已经发生并将相关参数传给订阅者就可以了。然后它就应该继续执行它后面的动作,在本例中就是打印“control back to client!”。而订阅者不管失败或是超时都不应该影响到发布者,但在上面的例子中,发布者却不得不等待订阅者的方法执行完毕才能继续运行。

现在我们来看下如何解决这个问题,先回顾一下之前我在c#中的委托和事件一文中提到的内容,我说过,委托的定义会生成继承自multicastdelegate的完整的类,其中包含invoke()、begininvoke()和endinvoke()方法。当我们直接调用委托时,实际上是调用了invoke()方法,它会中断调用它的客户端,然后在客户端线程上执行所有订阅者的方法(客户端无法继续执行后面代码),最后将控制权返回客户端。注意到begininvoke()、endinvoke()方法,在.net中,异步执行的方法通常都会配对出现,并且以begin和end作为方法的开头(最常见的可能就是stream类的beginread()和endread()方法了)。它们用于方法的异步执行,即是在调用begininvoke()之后,客户端从线程池中抓取一个闲置线程,然后交由这个线程去执行订阅者的方法,而客户端线程则可以继续执行下面的代码。

begininvoke()接受“动态”的参数个数和类型,为什么说“动态”的呢?因为它的参数是在编译时根据委托的定义动态生成的,其中前面参数的个数和类型与委托定义中接受的参数个数和类型相同,最后两个参数分别是asynccallback和object类型,对于它们更具体的内容,可以参见下一节委托和方法的异步调用部分。现在,我们仅需要对这两个参数传入null就可以了。另外还需要注意几点:

在委托类型上调用begininvoke()时,此委托对象只能包含一个目标方法,所以对于多个订阅者注册的情况,必须使用getinvocationlist()获得所有委托对象,然后遍历它们,分别在其上调用begininvoke()方法。如果直接在委托上调用begininvoke(),会抛出异常,提示“委托只能包含一个目标方法”。
如果订阅者的方法抛出异常,.net会捕捉到它,但是只有在调用endinvoke()的时候,才会将异常重新抛出。而在本例中,我们不使用endinvoke()(因为我们不关心订阅者的执行情况),所以我们无需处理异常,因为即使抛出异常,也是在另一个线程上,不会影响到客户端线程(客户端甚至不知道订阅者发生了异常,这有时是好事有时是坏事)。
begininvoke()方法属于委托定义所生成的类,它既不属于multicastdelegate也不属于delegate基类,所以无法继续使用可重用的fireevent()方法,我们需要进行一个向下转换,来获取到实际的委托类型。
现在我们修改一下上面的程序,使用异步调用来解决订阅者方法执行超时的情况:

class program6 {
static void main(string[] args) {

publisher pub = new publisher();
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(); // 触发事件

console.writeline("control back to client! "); // 返回控制权
console.writeline("press any thing to exit...");
console.readkey(); // 暂停客户程序,提供时间供订阅者完成方法
}
}

public class publisher {
public event eventhandler myevent;
public void dosomething() {
// 做某些其他的事情
console.writeline("dosomething invoked!");

if (myevent != null) {
delegate[] delarray = myevent.getinvocationlist();

foreach (delegate del in delarray) {
eventhandler method = (eventhandler)del;
method.begininvoke(null, eventargs.empty, null, null);
}
}
}
}

public class subscriber1 {
public void onevent(object sender, eventargs e) {
thread.sleep(timespan.fromseconds(3)); // 模拟耗时三秒才能完成方法
console.writeline("waited for 3 seconds, subscriber1 invoked!");
}
}

public class subscriber2 {
public void onevent(object sender, eventargs e) {
throw new exception("subsciber2 failed"); // 即使抛出异常也不会影响到客户端
//console.writeline("subscriber2 immediately invoked!");
}
}

public class subscriber3 {
public void onevent(object sender, eventargs e) {
thread.sleep(timespan.fromseconds(2)); // 模拟耗时两秒才能完成方法
console.writeline("waited for 2 seconds, subscriber3 invoked!");
}
}

运行上面的代码,会得到下面的输出:

dosomething invoked!
control back to client!

press any thing to exit...

waited for 2 seconds, subscriber3 invoked!
waited for 3 seconds, subscriber1 invoked!

需要注意代码输出中的几个变化:
当前2/3页 

如对本文有疑问,请在下面进行留言讨论,广大热心网友会与你互动!! 点击进行留言回复

相关文章:

验证码:
移动技术网