委托与事件 在.net的争霸战 ,你选择了谁?(异步委托产生的原因)

如果你对委托和事件尚有模糊的地方请参阅上2篇博文。

如果你对下面8个问题,可以轻而易举的回答,那博文对你没什么作用。

1.为什么在发布者与订阅者的模式中,我们使用了事件而不使用委托变量?

2.为什么我们通常的多播委托的返回类型都是void?

3.如何让事件只允许一个方法注册?

4.非void多播委托如何返回多个返回值?

5.当委托链表的注册方法异常时,如何解决?

6.如何解决事件中的委托方法的延时效果?

7.实现异步委托...?

8.保密

<磨刀>

理清思路:
委托 好比中介所,你在我这里注册了方法,我就代替你完成任务。
事件 好比微博,凡是收听我微博的人,只要我更新了微博(自己触发什么条件),收听我的人就会知道我更新了,以便做出自己的动作(评论/转发)。
即:事件必须自己触发。

 

<正文>
下面来解决疑问:
1.为什么在发布者与订阅者的模式中,我们使用了事件而不使用委托变量?

View Code
class Program
{
static void Main(string[] args)
{
Pub pub = new Pub();
Sub sub = new Sub();
pub.Display += sub.OnDisplay;
pub.ChangeNumber();
pub.Display(100);

}
}
public delegate void DisplayEventHandle(int number);
class Pub
{
public DisplayEventHandle Display;

public void ChangeNumber()
{
for (int i = 0; i < 10; i++)
{
if (i == 2)
{
if (Display != null)//如果注册方法
{
Display(i);
}
}
}
}
}

class Sub
{
public void OnDisplay(int i)
{
Console.WriteLine("The number is {0}",i);
}
}

分析:("="是赋值操作,"+="是注册方法)
.对于使用委托变量,那么在类的内部 委托变量(字段)必须是Public,这样不安全。
. pub.Display(100);事件本来是需要调用ChangeNumber()方法当i=2的时候触发的,然后订阅者作出一系列的动作,但是现在 pub.Display(100);委托自己就可以调用订阅者的动作,影响到了所有订阅者。

修改下代码:
 

View Code
class Program
{
static void Main(string[] args)
{
Pub pub = new Pub();
Sub sub = new Sub();
pub.Display += sub.OnDisplay;
pub.ChangeNumber();
//pub.Display(100); 报错

}
}
public delegate void DisplayEventHandle(int number);
class Pub
{
public event DisplayEventHandle Display;

public void ChangeNumber()
{
for (int i = 0; i < 10; i++)
{
if (i == 2)
{
if (Display != null)//如果注册方法
{
Display(i);
}
}
}
}
}

class Sub
{
public void OnDisplay(int i)
{
Console.WriteLine("The number is {0}",i);
}
}

分析:
.对象再也无法直接调用委托变量了,因为加了event事件,本质是生成了 私有private的委托。所以无法进行赋值操作,也不能直接调用。
.这样给客户端 少了一些使用类内部变量的权力。

2.为什么我们通常的多播委托的返回类型都是void?
对于单播委托,咱们不讨论了,太简单了。
对于多播委托:
委托变量 +=方法1;
委托变量 +=方法2;

试想下,咱们都知道委托变量的声明,参数和返回类型都是和方法一样的,那么委托调用方法的时候,如果有返回值,到底这个返回值是 方法1,还是方法2的。

比如:拿上面的微博案例来说,我是发布者,我可能今天心情不好,然后发布了一条微博,我根本不关心,谁收听我,也不关心收听者对我评论。即:事件发布者,他运行了某个动作之后,如果这个动作内的条件满足,那么就触发 订阅者的动作。事件发布者根本不需要关心,你订阅者的返回值。

但是,你要说,我就是想知道返回值是多少?好,我们做个测试:

View Code
class Program
{
static void Main(string[] args)
{
Pub pub = new Pub();
Sub1 sub1 = new Sub1();
Sub2 sub2 = new Sub2();
pub.Display += sub1.OnDisplay;
pub.Display += sub2.OnDisplay;
pub.ChangeNumber();//I am Sub2

}
}
public delegate string DisplayEventHandle();
class Pub
{
public event DisplayEventHandle Display;

public void ChangeNumber()
{
for (int i = 0; i < 10; i++)
{
if (i == 2)
{
if (Display != null)//如果注册方法
{
string str=Display();
Console.WriteLine(str);
}
}
}
}
}

class Sub1
{
public string OnDisplay()
{
return "I am Sub1";
}
}

class Sub2
{
public string OnDisplay()
{
return "I am Sub2";
}
}



答案是:最后一个订阅者的返回值。

3.如何让事件只允许一个方法注册?
可能有时候,我的微博只想让一个人收听,不想让 前任女友收听,怎么办?
从技术层面分析:
1.我们订阅的时候,如何追加订阅者 通过符号"+="是吧?但是我们说过,“=”符号也可以委托定义,从这个角度着手,可不可以让事件变成不可访问的(private),然后在内部利用“=”进行订阅,而不是让客户自己“+=”符号订阅。

View Code
class Program
{
static void Main(string[] args)
{
Pub pub = new Pub();
Sub1 sub1 = new Sub1();
Sub2 sub2 = new Sub2();
pub.AddDL(sub1.OnDisplay);
pub.AddDL(sub2.OnDisplay);
pub.Speaking();


}
}
public delegate string DisplayEventHandle();
class Pub
{
private event DisplayEventHandle Display;

public void AddDL(DisplayEventHandle method)
{
Display = method;//这里是"="不是"+="
}
public void RemoveDL(DisplayEventHandle method)
{
Display -= method;
}
public void Speaking()
{
if (Display != null)
{
string str = Display();
Console.WriteLine(str);
}
}
}

class Sub1
{
public string OnDisplay()
{
return "I am Sub1";
}
}

class Sub2
{
public string OnDisplay()
{
return "I am Sub2";
}
}


分析:
.有朋友可能会问,这样不是和刚才一样吗?带返回值的委托变量,其实不是的,当你使用+=的时候,2个方法都会被加入委托链表,而使用"="只是覆盖。
.有点类似于属性,对,就是 事件访问器。
如下代码:

View Code
class Program
{
static void Main(string[] args)
{
Pub pub = new Pub();
Sub1 sub1 = new Sub1();
Sub2 sub2 = new Sub2();
//pub.Display = sub1.OnDisplay;//错误,因为Display本质还是私有的委托,只是这里定义了2个委托
pub.Display += sub1.OnDisplay;
pub.Display += sub2.OnDisplay;//覆盖sub1方法
pub.Speaking();

}
}
public delegate string DisplayEventHandle();
class Pub
{
private DisplayEventHandle display;
public event DisplayEventHandle Display
{
add { display = value; }
remove { display -= value; }
}
public void Speaking()
{
if (display != null)
{
string str = display();
Console.WriteLine(str);
}
}
}

class Sub1
{
public string OnDisplay()
{
return "I am Sub1";
}
}

class Sub2
{
public string OnDisplay()
{
return "I am Sub2";
}
}



分析:
1.通过分析,发现事件的本质是 生成一个private的委托变量,这就是为什么 无法通过 事件变量=方法 操作的原因,因为这个变量是委托的变量无法访问。但是可以通过事件访问器访问,修改事件访问器可以限制方法进入 委托链表。


4.非void委托如何返回多个返回值?
思路:
1.我们知道 多播委托注册方法之后,会生产一个委托链表,那么我们可以把这个委托的注册方法遍历出来吗?

View Code
public delegate string DL();
class Program
{
static void Main(string[] args)
{
DL one = DoSomething;
one += Attacking;
//delegate[] dls; 这样是错误的,下面解释了
Delegate[] dlArray =one.GetInvocationList();
foreach(var n in dlArray)
{
Console.WriteLine(n.Method.Name);//遍历方法名字
}


}

static string DoSomething()
{
return "do it";
}

static string Attacking()
{
return "Attack";
}
}


首先,我们要先理清出一个概念:
delegate 与Delegate有什么区别?
Delegate:是一个抽象基类,它引用静态方法或引用类实例及该类的实例方法。然而,只有系统和编译器可以显式地从 Delegate 类派生出委托类型。
MulticastDelegate:是一个继承于Delegate的类,其拥有一个带有链表格式的委托列表,该列表称为调用列表,在调用多路广播委托时,将按照调用列表中的委托出现的顺序来同步调用这些委托。平常我们声明一个delegate的类型,都是继承于MulticastDelegate类的(注意:不能显式地从此类进行派生。这点与Delegate类是一样的,只有系统和编译器也可以显示地进行派生)。
delegate 是一个C#关键字,用来定义一个新的委托类型(继承自MulticastDelegate类)。

2.方法名遍历出来了,咱们利用List<>把每个方法的结果遍历出来

View Code
public delegate string DL();
class Program
{
static void Main(string[] args)
{
DL one = DoSomething;
one += Attacking;
//delegate[] dls; 这样是错误的,下面解释了
Delegate[] dlArray =one.GetInvocationList();
List<string> lists = new List<string>();
foreach(var n in dlArray)
{
Console.WriteLine(n.Method.Name);
DL newone = (DL)n;//把Delegate显示转换成DL类型,因为DL类的基类是Delegate类
string str = newone();
lists.Add(str);
}
foreach (var n in lists)
{
Console.WriteLine(n);
}
}
static string DoSomething()
{
return "do it";
}

static string Attacking()
{
return "Attack";
}
}



应用于 事件中:

View Code
class Program
{
static void Main(string[] args)
{
Pub pub = new Pub();
Sub1 sub1 = new Sub1();
Sub2 sub2 = new Sub2();
pub.Display +=new DisplayEventHandle(sub1.OnDisplay);//注册方法
pub.Display += new DisplayEventHandle(sub2.OnDisplay);//注册方法

List<string> lists=pub.Doing();//事件由发布者的某个条件触发
foreach (var n in lists)
{
Console.WriteLine(n);
}


}
}
public delegate string DisplayEventHandle();

class Pub //发布者
{
public event DisplayEventHandle Display;
List<string> lists = new List<string>();
Delegate[] dls;
public List<string> Doing()
{
if (Display != null)
{
dls = Display.GetInvocationList();
foreach (var n in dls)
{
Console.WriteLine(n.Method.Name);
DisplayEventHandle one=(DisplayEventHandle)n;
string str = one();
lists.Add(str);
}
}

return lists;
}

}

class Sub1//订阅者
{
public string OnDisplay()
{
return "I am Sub1";
}
}
class Sub2
{
public string OnDisplay()
{
return "I am Sub2";
}
}
}


事实上,发布者根不关心这些订阅者返回什么,它关心的是订阅者注册的方法是否正确,是否会报错,影响发布者的方法执行和后面订阅者方法的执行,所以 这种技术主要用于 返回 订阅者方法的异常处理信息。

5.当委托链表的注册方法异常时,如何解决?

源代码:

View Code
class Program
{
static void Main(string[] args)
{
Pub pub = new Pub();
Sub1 sub1 = new Sub1();
Sub2 sub2 = new Sub2();
pub.Display += new DisplayEventHandle(sub1.OnDisplay);//注册方法
pub.Display += new DisplayEventHandle(sub2.OnDisplay);//注册方法

pub.Doing();//事件由发布者的某个条件触发

}
}
public delegate string DisplayEventHandle(object sender, EventArgs e);

class Pub //发布者
{
public event DisplayEventHandle Display;

public void Doing()
{
if (Display != null)
{
Display(this,EventArgs.Empty);
}


}

}

class Sub1//订阅者
{
public string OnDisplay(object sender, EventArgs e)
{
return "I am Sub1";
}
}
class Sub2
{
public string OnDisplay(object sender, EventArgs e)
{
return "I am Sub2";
}
}



思考:如果订阅者方法异常了怎么办?对,我们利用try catch调试。
修改代码:

View Code
public void Doing()
{
if (Display != null)
{
try
{
string str=Display(this, EventArgs.Empty);
Console.WriteLine(str);
}
catch (Exception e)
{
Console.WriteLine(e.Message.ToString());
}
}
}

class Sub1//订阅者
{
public string OnDisplay(object sender, EventArgs e)
{
//return "I am Sub1";
throw new Exception("sub1方法异常了");
}
}



如果Sub1方法出了异常的话,那么就会终止 对 Sub2方法的调研,虽然 Doing()可以执行下去了。但是 影响了其他订阅者。
从这个层面思考,我们把 注册的方面 按照上面提到过的遍历一下,就能解决,因为在Foreach循环内当一个方法出了问题,只影响到问题方法本身。

修改代码如下:
 

View Code
class Program
{
static void Main(string[] args)
{
Pub pub = new Pub();
Sub1 sub1 = new Sub1();
Sub2 sub2 = new Sub2();
pub.Display +=new DisplayEventHandle(sub1.OnDisplay1);//注册方法
pub.Display += new DisplayEventHandle(sub2.OnDisplay2);//注册方法

List<string> lists=pub.Doing();//事件由发布者的某个条件触发
foreach (var n in lists)
{
Console.WriteLine(n);
}


}
}
public delegate string DisplayEventHandle(object sender,EventArgs e);

class Pub //发布者
{
public event DisplayEventHandle Display;
List<string> lists = new List<string>();
Delegate[] dls;
public List<string> Doing()
{
if (Display != null)
{
dls = Display.GetInvocationList();
foreach (var n in dls)
{
try
{
Console.WriteLine(n.Method.Name);
DisplayEventHandle one = (DisplayEventHandle)n;
string str = one(this, EventArgs.Empty);
lists.Add(str);
}
catch (Exception e)
{
Console.WriteLine(e.Message.ToString());
}
}
}

return lists;
}

}

class Sub1//订阅者
{
public string OnDisplay1(object sender,EventArgs e)
{
throw new Exception("Sub1方法异常了");
}
}
class Sub2
{
public string OnDisplay2(object sender, EventArgs e)
{
return "I am Sub2";
}
}



输出:
OnDisplay1
Sub1方法异常了
OnDisplay2
I am Sub2

总结:这样即知道了哪个方法异常了,又不影响其他订阅者调用自己的方法。


6.如何处理事件中的委托方法的超时?
上面可知,订阅者的注册方法如果有问题,会导致异常,然后影响到发布者的Doing()方法,还有一种让到发布者的Doing()方法经过很长时间执行的,就是超时。
但是超时不会影响发布者把 订阅者感兴趣的信息发布给订阅者,也不影响发布者的正常执行,只是执行Doing()会很长时间而已。

分析下:
1.发布者  执行某个动作的时候(事件由发布者自己触发),根据订阅者感兴趣的信息会调用 订阅者的注册方法(比如 当一个数字大于10的时候)。
2.我们按F11调试的时候都会发现,当触发事件的时候,就会转到 订阅者的内部方法上去,也就是说,当前线程在 执行 订阅者的方法,所以 Main函数内部的客户端就在等待方法执行完毕之后,才能继续下面的代码操作。

这里有点深度的:我举个例子
当Main函数的在执行一个发布者的方法的时候
比如 计算1-100的和,如果一个感兴趣的参数是和,当和大于10的时候,这个时候,线程就会转到 订阅者的方法上去,这个时候,客户端(Main函数的方法还能执行吗?)显然不可以继续执行了,必须等待订阅者执行完,才能继续下面的计算操作。

View Code
class Program
{
static void Main(string[] args)
{
Pub pub = new Pub();
Sub1 sub1 = new Sub1();
Sub2 sub2 = new Sub2();
pub.Display += new DisplayEventHandle(sub1.OnDisplay1);//注册方法
pub.Display += new DisplayEventHandle(sub2.OnDisplay2);//注册方法

List<string> lists=pub.Doing();//事件由发布者的某个条件触发
foreach (var n in lists)
{
Console.WriteLine(n);
}

Console.WriteLine("线程已经回到Main函数");


}
}
public delegate string DisplayEventHandle(object sender,EventArgs e);

class Pub //发布者
{
public event DisplayEventHandle Display;
List<string> lists = new List<string>();
Delegate[] dls;
public List<string> Doing()
{
if (Display != null)
{
dls = Display.GetInvocationList();
foreach (var n in dls)
{

Console.WriteLine("现在是方法:"+n.Method.Name);
DisplayEventHandle one = (DisplayEventHandle)n;
string str = one(this, EventArgs.Empty);
lists.Add(str);


}
}

return lists;
}

}

class Sub1//订阅者
{
public string OnDisplay1(object sender,EventArgs e)
{
Thread.Sleep(TimeSpan.FromSeconds(5));
return "线程已转到Sub1,等待5秒,Sub1方法执行";
}
}
class Sub2
{
public string OnDisplay2(object sender, EventArgs e)
{
return "线程已转到Sub2,Sub2方法执行。。。";
}
}



输出:
现在是方法:OnDisplay1
现在是方法:OnDisplay2
线程已转到Sub1,等待5秒,Sub1方法执行
线程已转到Sub2,Sub2方法执行。。。
线程已经回到Main函数


我们是发布者,我们需要是立刻输出:  线程已经回到Main函数,而订阅者的超时影响了我发布者的延迟输出。
还是拿微博来说:我是博主,我发微博就是抒发感情,和谁收听我,以及收听到我的信息没有以及如何对我做出反应都不关心。
但是现在,我必须 等待订阅者 方法结束了,我才可以操作,太让人生气了,为了解决这个问题。
怎么办?怎么办?IL看结构:
 


分析:
1.事件的本质我们知道,是生成 一个 private的委托变量
2.委托的本质我们知道,是生成 一个完整的继承与MulticastDelegate的类,委托本质是个类
这个类里包括:
BeginInvoke()、EndInvoke()、Invoke()3个方法。
我们记得要调用委托方法的时候是这样操作的:
委托变量();其实 实质就是 委托变量.Invoke();
对,就是这个方法是凶手,他妨碍了我们的发布者,让我们等待。

KO它,开始 异步委托

7.实现异步委托...?
异步就是 一个主线程执行了(Main函数),你要是委托调用方法,那是你的事情,你自己重新开辟新线程去搞,别影响我的主线程。
异步一般是 Begin 和End 出现。

1.BeginInvoke()执行时,从线程池抓取一个 "没事干的"的线程来替我去告诉 订阅者调用委托方法。
注:对于调用BeginInvoke()方法的时候,让线程去调用委托方法,这个委托变量必须只能有一个1个方法被绑定,如果是多播委托,必须像上面那么GetInvocationList()获得所有委托对象,先遍历出所有委托对象,再使用BeginInvoke()方法。
2.Main()继续自己的线程执行下面的工作
3.EndInvoke();当订阅者方法异常的时候,我们知道可以try catch捕获,但是只有在EndInvoke()才会抛出。(其实发布者并不关心这些抛出异常的信息),并且抛出异常也是在另一个进程上。

好了,开始写代码:

View Code
class Program
{
static void Main(string[] args)
{
Pub pub = new Pub();
Sub1 sub1 = new Sub1();
Sub2 sub2 = new Sub2();
pub.Display += new DisplayEventHandle(sub1.OnDisplay);//注册方法
pub.Display += new DisplayEventHandle(sub2.OnDisplay);//注册方法

pub.Doing();//事件由发布者的某个条件触发
Console.WriteLine("线程还在Main()");

Console.ReadKey();//为什么这样写,因为主线程是Main函数,当他执行完之后,程序就结束了,可能子线程还没结束呢

}
}
public delegate void DisplayEventHandle(object sender, EventArgs e);

class Pub //发布者
{
public event DisplayEventHandle Display;

public void Doing()
{
if (Display != null)
{
Delegate[] dels = Display.GetInvocationList();
foreach (var n in dels)
{
DisplayEventHandle newone = (DisplayEventHandle)n;
IAsyncResult result = newone.BeginInvoke(this, EventArgs.Empty, null, null);//新开辟一个线程

//newone.EndInvoke(result);//加上这个效果如何?效果还是需要等待订阅者方法的结果,所以这个是没要添加的

}
}


}

}

class Sub1//订阅者
{
public void OnDisplay(object sender, EventArgs e)
{
Thread.Sleep(TimeSpan.FromSeconds(3));
Console.WriteLine("Sub1线程");

}
}
class Sub2
{
public void OnDisplay(object sender, EventArgs e)
{
Console.WriteLine("Sub2线程");

}
}
}



输出结果:
线程还在Main()
Sub2线程
Sub1线程

总结:和我们预期的一样效果,注意上面的 Console.ReadKey(),不写这个,程序运行完成将导致子线程的输出无法完成。可能难以理解
分析下:
1.对于主线程就是 Main()函数,对于子线程分为 前台线程和后台线程,我们这里的就是后台线程,对于后台线程,只要主线程运行结束程序就结束了,不会管这些后台线程,但是对于前台线程注意了,必须前台线程结束了,主线程才会结束。

线程这块知识暂时不讨论了,会有专门的博文发布。
2.这里是 并行执行的,不要以为因为Foreach遍历就会先执行Sub1,其实是2个一起执行的。也就是说 后台线程最长利用了3秒钟。


哈哈,是不是觉得异步调用委托方法学完了?错,这才刚刚开始:
注意到没,上面有一个Console.ReadKey();
必须输入一个 键,程序才会退出,所以异步调用就需要更多的控制,比如当后台线程执行完毕了,自动告诉客户端,我结束了,可以关闭程序了这类问题,比如 客户端需要 后台线程 执行的结果。

总结:带着这些问题,将在明天发布《线程与异步调用委托方法的渊源》

posted @ 2012-02-09 16:14  Anleb  阅读(3929)  评论(19编辑  收藏  举报