C# 委托:一个睡前故事






4.94/5 (248投票s)
2001年12月4日
6分钟阅读

360291

2
一个以睡前故事风格讲述的、为 C# 程序员准备的关于委托和事件的探索性故事
紧耦合
很久很久以前,在一个离这里很远的奇怪国度里,住着一个名叫彼得的工人。他是一个勤奋的工人,总是乐于接受老板的请求。然而,他的老板是一个刻薄、不信任人的家伙,坚持要求定期的进度报告。由于彼得不想让老板站在他的办公室里盯着他,彼得答应在工作取得进展时通知老板。彼得通过定期通过键入的引用回调老板来履行这一承诺,如下所示:
class Worker {
public void Advise(Boss boss) { _boss = boss; }
public void DoWork() {
Console.WriteLine("Worker: work started");
if( _boss != null ) _boss.WorkStarted();
Console.WriteLine("Worker: work progressing");
if( _boss != null ) _boss.WorkProgressing();
Console.WriteLine("Worker: work completed");
if( _boss != null ) {
int grade = _boss.WorkCompleted();
Console.WriteLine("Worker grade= " + grade);
}
}
private Boss _boss;
}
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();
}
}
接口
彼得是个特别的人。他不仅能忍受他那刻薄的老板,而且与周围的宇宙有着深厚的联系。如此之深,以至于他觉得宇宙对他取得的进展很感兴趣。不幸的是,彼得无法通知宇宙他的进展,除非他添加一个特殊的 Advise
方法和特殊的、专门为宇宙准备的回调,同时还要向老板汇报。彼得真正想做的是将潜在通知的列表与这些通知方法的实现分离开来。于是,他决定将方法分解成一个接口:
interface IWorkerEvents {
void WorkStarted();
void WorkProgressing();
int WorkCompleted();
}
class Worker {
public void Advise(IWorkerEvents events) { _events = events; }
public void DoWork() {
Console.WriteLine("Worker: work started");
if( _events != null ) _events.WorkStarted();
Console.WriteLine("Worker: work progressing");
if(_events != null ) _events.WorkProgressing();
Console.WriteLine("Worker: work completed");
if(_events != null ) {
int grade = _events.WorkCompleted();
Console.WriteLine("Worker grade= " + grade);
}
}
private IWorkerEvents _events;
}
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 */
}
}
委托(Delegates)
不幸的是,彼得忙于说服他的老板实现这个接口,以至于他还没来得及通知宇宙,但他知道他很快就会做到。至少他已经将他老板的引用从他身边抽象出来了,这样其他实现 IWorkerEvents
接口的人就可以被通知到他的工作进展了。
尽管如此,他的老板还是愤怒地抱怨。“彼得!”老板咆哮道。“你为什么要在我开始工作或工作正在进行时通知我?!我根本不在乎这些事件。你不仅强迫我实现这些方法,还在我等待从事件返回时浪费宝贵的工作时间,而当我身处远方时,这只会进一步延长!你就不能想个办法别烦我了吗?”
于是,彼得决定,虽然接口在很多方面很有用,但当涉及到事件时,它们的粒度不够细。他希望能够只通知感兴趣的各方符合他们内心愿望的事件。于是,他决定将接口中的方法分解成独立的 delegate
函数,每个函数就像一个只包含一个方法的微小接口。
delegate void WorkStarted();
delegate void WorkProgressing();
delegate int WorkCompleted();
class Worker {
public void DoWork() {
Console.WriteLine("Worker: work started");
if( started != null ) started();
Console.WriteLine("Worker: work progressing");
if( progressing != null ) progressing();
Console.WriteLine("Worker: work completed");
if( completed != null ) {
int grade = completed();
Console.WriteLine("Worker grade= " + grade);
}
}
public WorkStarted started;
public WorkProgressing progressing;
public WorkCompleted completed;
}
class Boss {
public int WorkCompleted() {
Console.WriteLine("Better...");
return 4; /* out of 10 */
}
}
class Universe {
static void Main() {
Worker peter = new Worker();
Boss boss = new Boss();
peter.completed = new WorkCompleted(boss.WorkCompleted);
peter.DoWork();
Console.WriteLine("Main: worker completed work");
Console.ReadLine();
}
}
静态监听器
这实现了不打扰老板不需要的事件的目标,但彼得仍然没有设法将宇宙添加到他的监听器列表中。由于宇宙是一个包罗万象的实体,将委托连接到实例成员似乎不合适(想象一下多个宇宙实例需要多少资源……)。相反,彼得需要将 delegate
连接到 static
成员,而 delegate
完全支持这一点。
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 = new WorkCompleted(boss.WorkCompleted);
peter.started = new WorkStarted(Universe.WorkerStartedWork);
peter.completed = new WorkCompleted(Universe.WorkerCompletedWork);
peter.DoWork();
Console.WriteLine("Main: worker completed work");
Console.ReadLine();
}
}
事件
不幸的是,宇宙非常忙碌,不习惯关注个体,它设法用自己的 delegate
替换了彼得老板的 delegate
。这是彼得的 Worker
类将 delegate
字段设为 public
的一个意外副作用。同样,如果彼得的老板变得不耐烦,他可以自己解雇彼得的 delegate
(这正是彼得老板可能做的粗鲁事情)。
// Peter's boss taking matters into his own hands
if( peter.completed != null ) peter.completed();
彼得想确保这两种情况都不会发生。他意识到他需要为每个 delegate
添加注册和注销函数,以便监听器可以添加或删除自己,但不能清除整个列表或触发彼得的事件。他没有自己实现这些函数,而是使用了 event
关键字,让 C# 编译器为他构建这些方法。
class Worker {
...
public event WorkStarted started;
public event WorkProgressing progressing;
public event WorkCompleted completed;
}
彼得知道 event
关键字会在 delegate
周围创建一个属性,只允许 C# 客户端使用 +=
和 -=
运算符添加或删除自己,迫使他的老板和宇宙好好相处。
static void Main() {
Worker peter = new Worker();
Boss boss = new Boss();
peter.completed += new WorkCompleted(boss.WorkCompleted);
peter.started += new WorkStarted(Universe.WorkerStartedWork);
peter.completed += new WorkCompleted(Universe.WorkerCompletedWork);
peter.DoWork();
Console.WriteLine("Main: worker completed work");
Console.ReadLine();
}
收集所有结果
此时,彼得如释重负。他设法满足了所有监听器的要求,而无需与特定的实现紧密耦合。然而,他注意到,虽然他的老板和宇宙都提供了他的工作成绩,但他只收到了其中一个成绩。面对多个监听器,他真的很想收集所有人的结果。于是,他伸手去拿他的委托,取出监听器列表,以便他可以手动调用他们所有人。
public void DoWork() {
...
Console.WriteLine("Worker: work completed");
if( completed != null ) {
foreach( WorkCompleted wc in completed.GetInvocationList() ) {
int grade = wc();
Console.WriteLine("Worker grade= " + grade);
}
}
}
异步通知:发送和遗忘
与此同时,他的老板和宇宙被其他事情分散了注意力,这意味着他们对彼得工作的评分所需的时间大大延长了。
class Boss {
public int WorkCompleted() {
System.Threading.Thread.Sleep(3000);
Console.WriteLine("Better..."); return 6; /* out of 10 */
}
}
class Universe {
static int WorkerCompletedWork() {
System.Threading.Thread.Sleep(4000);
Console.WriteLine("Universe is pleased with worker's work");
return 7;
}
...
}
不幸的是,由于彼得是一次一个地通知监听器,等待他们每个人给他评分,这些通知现在占用了他很多时间,而他本应该在工作。于是,他决定不再顾及评分,而是异步发送事件。
public void DoWork() {
...
Console.WriteLine("Worker: work completed");
if( completed != null ) {
foreach( WorkCompleted wc in completed.GetInvocationList() )
{
wc.BeginInvoke(null, null);
}
}
}
异步通知:轮询
这使得彼得在通知监听器的同时还能立即回到工作岗位,让进程线程池调用 delegate
。然而,随着时间的推移,彼得发现他怀念对工作的反馈。他知道自己做得很好,并且赞赏整个宇宙的赞美(即使不是他老板的)。于是,他异步发送事件,但会定期轮询,查看评分是否可用。
public void DoWork() {
...
Console.WriteLine("Worker: work completed");
if( completed != null ) {
foreach( WorkCompleted wc in completed.GetInvocationList() ) {
IAsyncResult res = wc.BeginInvoke(null, null);
while( !res.IsCompleted ) System.Threading.Thread.Sleep(1);
int grade = wc.EndInvoke(res);
Console.WriteLine("Worker grade= " + grade);
}
}
}
异步通知:委托
不幸的是,彼得又回到了他最初希望老板避免的事情上,即监视工作实体。于是,他决定使用自己的 delegate
作为当 async delegate
完成时的通知方式,让他能够立即回到工作岗位,但仍然能够在他的工作被评分时得到通知。
public void DoWork() {
...
Console.WriteLine("Worker: work completed");
if( completed != null ) {
foreach( WorkCompleted wc in completed.GetInvocationList() ) {
wc.BeginInvoke(new AsyncCallback(WorkGraded), wc);
}
}
}
private void WorkGraded(IAsyncResult res) {
WorkCompleted wc = (WorkCompleted)res.AsyncState;
int grade = wc.EndInvoke(res);
Console.WriteLine("Worker grade= " + grade);
}
宇宙的幸福
彼得、他的老板和宇宙终于满意了。彼得的老板和宇宙被允许被通知他们感兴趣的事件,从而减轻了实现负担和不必要的往返成本。彼得可以单独通知他们,而不用管他们从目标方法返回需要多长时间,同时仍然能异步地获得他的结果。彼得知道,事情并没有那么简单,因为一旦他异步地发送事件,目标方法很可能会在另一个线程上执行,就像彼得的通知目标方法何时完成一样。然而,Peter 是 Mike 的好朋友,Mike 非常熟悉线程问题,可以在这方面提供指导。
许可证
本文未附加明确的许可证,但可能在文章文本或下载文件本身中包含使用条款。如有疑问,请通过下面的讨论区联系作者。
作者可能使用的许可证列表可以在此处找到。