故事发生在2001年,至今20年了,感谢作者!让抽象的技术与现实场景完美融合,如此美妙,可遇不可求!
原址:.NET Delegates: A C# Bedtime Story
The following is an excerpt from Windows Forms 2.0 Programming, Chris Sells & Michael Weinhardt, Addison-Wesley, 2006. It's been updated from the original version for C# 2.0.
从前,在这南边一块陌生的土地上,有一个名叫彼得的工人。他为人勤快,对老板言听计从。然而,他的老板是一个吝啬鬼,不信任别人,并坚持要彼得定期汇报工作。彼得不想让他的老板站在办公室整天监视他,所以就答应一有工作进展就通知他的老板。Peter履行了承诺,并通过一个类型引用定期call back给他的老板,如下:
class Worker {
Boss boss;
public void Advise(Boss boss) {
this.boss = boss;
}
public void DoWork() {
Console.WriteLine("Worker: work started");
if( this.boss != null ) this.boss.WorkStarted();
Console.WriteLine("Worker: work progressing");
if( this.boss != null ) this.boss.WorkProgressing();
Console.WriteLine("Worker: work completed");
if( this.boss != null ) {
int grade = this.boss.WorkCompleted();
Console.WriteLine("Worker grade= {0}", grade);
}
}
}
class Boss {
public void WorkStarted() {
// Boss doesn't care
}
public void WorkProgressing() {
// Boss doesn't care
}
public int WorkCompleted() {
Console.WriteLine("It's about time!");
return 2; // out of 10
}
}
class Universe {
static void Main() {
Worker peter = new Worker();
Boss boss = new Boss();
peter.Advise(boss);
peter.DoWork();
Console.WriteLine("Main: worker completed work");
Console.ReadLine();
}
}
现在彼得是个很特别的人,不仅能忍受老板的小气,还与宇宙有着深刻的联系(but he also had a deep connection with the universe around him)(这个universe不清楚是不是这个意思)。以至于他觉得宇宙都在关注他的进步。不幸的是,除了让自己的老板知道,他没办法向外界宣告他的进展,除非他给宇宙添加一个特殊的建议方法和特殊的回调。Peter真正想做的是将潜在的通知列表从这些通知方法的实现中分离出来。所以他决定将这些方法拆分为一个接口:
interface IWorkerEvents {
void WorkStarted();
void WorkProgressing();
int WorkCompleted();
}
class Worker {
IWorkerEvents events;
public void Advise(IWorkerEvents events) {
this.events = events;
}
public void DoWork() {
Console.WriteLine("Worker: work started");
if( this.events != null ) this.events.WorkStarted();
Console.WriteLine("Worker: work progressing");
if( this.events != null ) this.events.WorkProgressing();
Console.WriteLine("Worker: work completed");
if( this.events!= null ) {
int grade = this.events.WorkCompleted();
Console.WriteLine("Worker grade= {0}", grade);
}
}
}
class Boss : IWorkerEvents {
public void WorkStarted() {
// Boss doesn't care
}
public void WorkProgressing() {
// Boss doesn't care
}
public int WorkCompleted() {
Console.WriteLine("It's about time!");
return 3; // out of 10
}
}
不幸的是,Peter正忙着说服他的老板实现这个接口,他没有抽出时间通知宇宙,但他知道很快就会通知的。至少他已经将Boss的引用抽象到远离他的地方,这样其他实现IWorkerEvents接口的人就可以得到他工作进度的通知。
尽管如此,他的老板还是忿忿地抱怨。
“彼得!” ,他的老板吼道:“为什么你要在开始工作、工作进展的时候通知我?!?我不在乎那些事件。你不仅强迫我去弄这些事儿,还特别浪费我宝贵的时间,如果我不在,事件就越来越多!你就不能想想办法不再烦我吗?”
因此,Peter认为虽然接口在很多方面都很有用,但在事件方面,它们的粒度还不够好。他希望能够只将符合内心所需的事件通知给对方。因此,他决定将接口中的方法分解为独立的委托函数,每个委托就像一个小接口:
delegate void WorkStarted();
delegate void WorkProgressing();
delegate int WorkCompleted();
class Worker {
public WorkStarted Started;
public WorkProgressing Progressing;
public WorkCompleted Completed;
public void DoWork() {
Console.WriteLine("Worker: work started");
if( this.Started != null ) this.Started();
Console.WriteLine("Worker: work progressing");
if( this.Progressing != null ) this.Progressing();
Console.WriteLine("Worker: work completed");
if( this.Completed != null ) {
int grade = this.Completed();
Console.WriteLine("Worker grade= {0}", grade);
}
}
}
class Boss {
public int WorkCompleted() {
Console.WriteLine("It's about time!");
return 4; // out of 10
}
}
class Universe {
static void Main() {
Worker peter = new Worker();
Boss boss = new Boss();
// NOTE: We've replaced the Advise method with the assignment operation
peter.Completed = new WorkCompleted(boss.WorkCompleted);
peter.DoWork();
Console.WriteLine("Main: worker completed work");
Console.ReadLine();
}
}
而且,由于Peter承受着巨大的压力,他决定使用C#2.0提供的委托简写语法:
class Universe {
static void Main() {
...
peter.Completed = boss.WorkCompleted;
...
}
}
委托完成了不让老板为他不想发生的事情而烦恼的目标,但彼得仍然没有把宇宙列入他的听众名单。因为宇宙是一个包罗万象的实体,将委托与实例成员挂钩似乎是不对的(想象一下宇宙的多个实例将需要多少资源……)。相反,Peter需要将委托与静态成员挂钩,而静态成员完全支持:
class Universe {
static void WorkerStartedWork() {
Console.WriteLine("Universe notices worker starting work");
}
static int WorkerCompletedWork() {
Console.WriteLine("Universe pleased with worker's work");
return 7;
}
static void Main() {
Worker peter = new Worker();
Boss boss = new Boss();
peter.Completed = boss.WorkCompleted;
peter.Started = WorkerStartedWork;
peter.Completed = WorkerCompletedWork; // Oops!
peter.DoWork();
Console.WriteLine("Main: worker completed work");
Console.ReadLine();
}
}
不幸的是,由于宇宙非常繁忙,不习惯关注个人,已经尝试用自己的delegate代替了彼得老板的delegate。这是在Peter的Worker类中公开委托字段的意外副作用。同样,如果彼得的老板变得不耐烦,他可以决定自己解雇彼得的delegate(这正是彼得老板经常用的损招):
// Peter's boss taking matters into his own hands
if( peter.Completed != null ) peter.Completed();
彼得想确保这两种情况都不会发生。他意识到他需要为每个代理添加注册和注销函数,以便监听者可以添加或删除自己,但不能清除整个列表或触发Peter的事件。Peter没有亲自实现这些函数,而是使用event关键字让C#编译器为他构建以下方法:
class Worker {
public event WorkStarted Started;
public event WorkProgressing Progressing;
public event WorkCompleted Completed;
...
}
Peter知道event关键字在委托周围建立了一个属性,只允许客户端添加或删除自己(使用C#中的+=和-=运算符),这迫使他的老板和整个世界玩得很嗨:
class Universe {
...
static void Main() {
Worker peter = new Worker();
Boss boss = new Boss();
peter.Completed = boss.WorkCompleted; // ERR!
peter.Completed += boss.WorkCompleted; // OK
peter.Started += Universe.WorkerStartedWork; // OK
peter.Completed += Universe.WorkerCompletedWork; // OK
peter.DoWork();
Console.WriteLine("Main: worker completed work");
Console.ReadLine();
}
}
这时,彼得松了一口气。他成功地满足了所有监听者(listeners)的需求,而不必与特定的实现紧密耦合。然而,他注意到,当他的老板和宇宙都给他的工作打分时,他只收到了其中的一个分数。面对众多监听者,他真的很想收获他们所有的成果。于是,他打开委托,拿出监听者列表,这样他就可以手动调用每一个监听者了:
class Worker {
...
public void DoWork() {
...
Console.WriteLine("Worker: work completed");
if( this.Completed != null ) {
foreach( WorkCompleted wc in this.Completed.GetInvocationList() ) {
int grade = wc();
Console.WriteLine("Worker grade= {0}", grade);
}
}
}
}
与此同时,他的老板和整个宇宙都被其他事情分散了注意力,这意味着他们给彼得的作业打分的时间大大延长了:
class Boss {
public int WorkCompleted() {
System.Threading.Thread.Sleep(5000);
Console.WriteLine("Better...");
return 4; // out of 10
}
}
class Universe {
...
static int WorkerCompletedWork() {
System.Threading.Thread.Sleep(1000000);
Console.WriteLine("Universe pleased with worker's work");
return 7;
}
...
}
不幸的是,由于Peter每次只通知一个听众,等待每个听众给他打分,这些通知占用了他本该工作的相当多的时间。所以,他决定忘记等级,只是异步地触发事件:
class Worker {
...
public void DoWork() {
...
Console.WriteLine("Worker: work completed");
if( this.Completed != null ) {
foreach( WorkCompleted wc in this.Completed.GetInvocationList() ) {
wc.BeginInvoke(null, null); // EndInvoke call required by .NET
}
}
}
}
对BeginInvoke的调用允许Peter通知监听者,同时让Peter立即返回工作,让进程线程池调用委托。然而,随着时间的推移,彼得发现他错过了对自己工作的反馈。他知道自己的工作做得很好,并感激整个宇宙的赞扬(如果不是他的老板的话)。另外,他担心他会泄漏通过调用BeginInvoke而没有调用相应的EndInvoke方法而获得的.NET资源,因此,他异步地触发事件,但定期轮询,寻找可用的级别:
class Worker {
...
public void DoWork() {
...
Console.WriteLine("Worker: work completed");
if( this.Completed != null ) {
foreach( WorkCompleted wc in this.Completed.GetInvocationList() ) {
IAsyncResult result = wc.BeginInvoke(null, null);
while( !result.IsCompleted ) System.Threading.Thread.Sleep(1);
int grade = wc.EndInvoke(result);
Console.WriteLine("Worker grade= {0}", grade);
}
}
}
}
不幸的是,Peter又回到了一开始他想让老板避免的事情上,也就是老板总在背后盯着他。因此,当异步工作完成时,他决定使用自己的委托作为通知方式,让他立即回到工作中,但在工作评分后仍会收到通知:
class Worker {
...
public void DoWork() {
...
Console.WriteLine("Worker: work completed");
if( this.Completed != null ) {
foreach( WorkCompleted wc in this.Completed.GetInvocationList() ) {
wc.BeginInvoke(this.WorkGraded, wc);
}
}
}
void WorkGraded(IAsyncResult result) {
WorkCompleted wc = (WorkCompleted)result.AsyncState;
int grade = wc.EndInvoke(result);
Console.WriteLine("Worker grade= {0}" + grade);
}
}
此时,Peter正在使用委托通知他的工作过程中感兴趣的各方,并使用委托在完成工作的分数可用时获得通知。他的老板和整个宇宙提供的委托是由单独的实体提供的,因此将它们封装在这些实体的方法中是有意义的。然而,在WorkGraded方法的情况下,除了c# 1.0的语法要求外,真的没有什么好的理由让它成为一个单独的方法。从c# 2.0开始,Peter可以将处理其工作成绩所需的代码放入匿名委托中:
class Worker {
...
public void DoWork() {
...
Console.WriteLine("Worker: work completed");
if( this.Completed != null ) {
foreach( WorkCompleted wc in this.Completed.GetInvocationList() ) {
wc.BeginInvoke(delegate(IAsyncResult result) {
WorkCompleted wc2 = (WorkCompleted)result.AsyncState;
int grade = wc2.EndInvoke(result);
Console.WriteLine("Worker grade= {0}", grade);
},
wc);
}
}
}
}
在这里,他不是在工作评分后传入要调用的方法的名称,而是传递方法体本身,通过使用delegate关键字的不同用法来创建一个没有名称的方法(因此是“匿名”)。该方法的主体基本上是相同的,因为Peter仍然将WorkCompleted委托作为参数传递给BeginInvoke,然后从AsyncState中提取它以用于提取结果。然而,Peter所知道的匿名委托的好处之一是,他可以从匿名委托主体中利用周围环境中的变量,从而重写代码:
class Worker {
...
public void DoWork() {
...
Console.WriteLine("Worker: work completed");
if( this.Completed != null ) {
foreach( WorkCompleted wc in this.Completed.GetInvocationList() ) {
wc.BeginInvoke(delegate(IAsyncResult result) {
// Use wc variable from surrounding context (ERR!)
int grade = wc.EndInvoke(result);
Console.WriteLine("Worker grade= {0}", grade);
},
null);
}
}
}
}
这段代码编译正常,但运行时,会引发以下异常:
System.InvalidOperationException:
The IAsyncResult object provided does not match this delegate.
问题是,虽然允许在匿名委托中使用wc变量,但foreach语句仍然在使用它。一旦异步调用开始,wc变量就会改变,用于启动事务的委托(wc)不再匹配作为参数传递给匿名委托的异步结果。彼得一拍脑袋,整出一个混合解决方案:
class Worker {
...
public void DoWork() {
...
Console.WriteLine("Worker: work completed");
if( this.Completed != null ) {
foreach( WorkCompleted wc in this.Completed.GetInvocationList() ) {
// Create an unchanging variable referencing the current delegate
WorkCompleted wc2 = wc;
wc.BeginInvoke(delegate(IAsyncResult result) {
// Use wc2 variable from surrounding context
int grade = wc2.EndInvoke(result);
Console.WriteLine("Worker grade= {0}", grade);
},
null);
}
}
}
}
彼得,他的老板和宇宙终于满意了。Peter的老板和整个宇宙可以被告知他们感兴趣的事件,从而减少了实施负担和不必要的往返成本。Peter可以通知他们每个人,忽略他们从目标方法返回所需的时间,同时仍然异步地获得他的结果,并使用匿名委托处理它们,从而得到以下完整的解决方案:
delegate void WorkStarted();
delegate void WorkProgressing();
delegate int WorkCompleted();
class Worker {
public event WorkStarted Started;
public event WorkProgressing Progressing;
public event WorkCompleted Completed;
public void DoWork() {
Console.WriteLine("Worker: work started");
if( this.Started != null )
this.Started();
Console.WriteLine("Worker: work progressing");
if( this.Progressing != null )
this.Progressing();
Console.WriteLine("Worker: work completed");
if( this.Completed != null ) {
foreach( WorkCompleted wc in this.Completed.GetInvocationList() ) {
WorkCompleted wc2 = wc;
wc.BeginInvoke(delegate(IAsyncResult result) {
int grade = wc2.EndInvoke(result);
Console.WriteLine("Worker grade= {0}", grade);
},
null);
}
}
}
}
class Boss {
public int WorkCompleted() {
System.Threading.Thread.Sleep(3000);
Console.WriteLine("Better...");
return 5; // out of 10
}
}
class Universe {
static void WorkerStartedWork() {
Console.WriteLine("Universe notices worker starting work");
}
static int WorkerCompletedWork() {
System.Threading.Thread.Sleep(4000);
Console.WriteLine("Universe pleased with worker's work");
return 7;
}
static void Main() {
Worker peter = new Worker();
Boss boss = new Boss();
peter.Completed += boss.WorkCompleted;
peter.Started += Universe.WorkerStartedWork;
peter.Completed += Universe.WorkerCompletedWork;
peter.DoWork();
Console.WriteLine("Main: worker completed work");
}
}
Peter知道异步获取结果会带来问题,因为一旦以异步方式触发事件,目标方法很可能会在另一个线程上执行,正如Peter通知目标方法何时完成一样。然而,Peter和Mike是好朋友,Mike非常熟悉线程问题,可以在该领域提供指导。
从此,他们都过着幸福的生活。
End。