当前位置: 首页 > 工具软件 > Bedtime > 使用案例 >

[译].NET Delegates: A C# Bedtime Story(.NET委托:一个C#睡前故事)

邓欣可
2023-12-01

故事发生在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();
  }
}

Interfaces

接口

         现在彼得是个很特别的人,不仅能忍受老板的小气,还与宇宙有着深刻的联系(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
  }
}

Delegates

委托

        不幸的是,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;
    ...
  }
}

Static Listeners

静态监听者

        委托完成了不让老板为他不想发生的事情而烦恼的目标,但彼得仍然没有把宇宙列入他的听众名单。因为宇宙是一个包罗万象的实体,将委托与实例成员挂钩似乎是不对的(想象一下宇宙的多个实例将需要多少资源……)。相反,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();
  }
}

Events

事件

        不幸的是,由于宇宙非常繁忙,不习惯关注个人,已经尝试用自己的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();
  }
}

Harvesting All Results

收获所有成果

        这时,彼得松了一口气。他成功地满足了所有监听者(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);
      }
    }
  }
}

Asynchronous Notification: Fire & Forget

异步通知:Fire & Forget

与此同时,他的老板和整个宇宙都被其他事情分散了注意力,这意味着他们给彼得的作业打分的时间大大延长了:

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
      }
    }
  }
}

Asynchronous Notification: Polling

异步通知:轮询

        对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);
      }
    }
  }
}

Asynchronous Notification: Delegates

异步通知:委托

不幸的是,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);
  }
}

Anonymous Delegates

匿名委托

        此时,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);
      }
    }
  }
}

Happiness in the Universe

宇宙中的幸福

彼得,他的老板和宇宙终于满意了。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。

 类似资料: