Delegate那点事
1,紧耦合
从前,在一块奇异的土地上,有个工人名叫彼得,他非常勤奋,对他的老板总是百依百顺。但是他的老板是个吝啬的人,从不信任别人,坚决要求随时知道彼得的工作进度,以防止他偷懒。但是彼得又不想让老板呆在他的办公室里站在背后盯着他,于是就对老板做出承诺:无论何时,只要我的工作取得了一点进展我都会及时让你知道。彼得通过周期性地使用“回调”他的老板来实现他的承诺,如下:
class Worker {
public void Advise(Boss boss) { _boss = boss; }
public void DoWork() {
Console.WriteLine(“工作:工作开始”);
if( _boss != null )_boss.WorkStarted();
Console.WriteLine(“工作: 工作进行中”);
if( _boss != null )_boss.WorkProgressing();
Console.WriteLine("“工作: 工作完成”");
if( _boss != null ) {
int grade =_boss.WorkCompleted();
Console.WriteLine(“工人的工作得分=” + grade);
}
}
private Boss _boss;
}
classBoss {
public void WorkStarted() { /* 老板不关心。*/ }
public void WorkProgressing() { /*老板不关心。*/ }
public int WorkCompleted() {
Console.WriteLine(“时间差不多!”);
return 2; /* 总分为10*/
}
}
classUniverse {
static void Main() {
Worker peter = new Worker();
Boss boss = new Boss();
peter.Advise(boss);
peter.DoWork();
Console.WriteLine(“Main: 工人工作完成”);
Console.ReadLine();
}
}
2.接口
现在,彼得成了一个特殊的人,他不但能容忍吝啬的老板,而且和他周围的宇宙也有了密切的联系,以至于他认为宇宙和周围的其他事物也会对他的工作进度也感兴趣。
不幸的是,他必须也给宇宙或周围的事物添加一个回调函数Advise来实现同时向他老板和宇宙报告工作进度。
彼得想要把潜在的通知的列表和这些通知的实现方法分离开来,于是他决定把方法分离为一个接口:
interfaceIWorkerEvents {
void WorkStarted();
void WorkProgressing();
int WorkCompleted();
}
classWorker {
public void Advise(IWorkerEvents events) { _events = events;}
public void DoWork() {
Console.WriteLine(“工作:工作开始”);
if( _events != null )_events.WorkStarted();
Console.WriteLine(“工作: 工作进行中”);
if(_events != null )_events.WorkProgressing();
Console.WriteLine("“工作: 工作完成”");
if(_events != null ) {
int grade =_events.WorkCompleted();
Console.WriteLine(“工人的工作得分=” + grade);
}
}
private IWorkerEvents _events;
}
classBoss : IWorkerEvents {
public void WorkStarted() { /* 老板不关心。*/ }
public void WorkProgressing() { /* 老板不关心。*/ }
public int WorkCompleted() {
Console.WriteLine(“时间差不多!”);
return 3; /* 总分为10*/
}
}
classCollage : IWorkerEvents {
public void WorkStarted() { /* 同事不关心。?*/ }
public void WorkProgressing() { /*同事关心。?不能一起打牌了*/ }
public int WorkCompleted() {
Console.WriteLine(“太好了!下班了”);
return 3; /* 总分为10,不关心评分*/
}
}
classUniverse {
static void Main() {
Worker peter = new Worker();
Boss boss = new Boss();
peter.Advise(boss);
peter.DoWork();
Console.WriteLine(“Main: 工人工作完成”);
Console.ReadLine();
}
}
不幸的是,每当彼得忙于通过接口的实现和老板交流时,就没有机会及时通知宇宙和同事了。(其实现在他也不知道怎么通知宇宙,呼叫上帝???)
至少他应该忽略身在远方的老板的引用,好让其他实现了IWorkerEvents的对象得到他的工作报告。
3,委托
他的老板还是抱怨得很厉害。“彼得!”他老板吼道,“你为什么在工作一开始和工作进行中都来烦我?!我不关心这些事件。你不但强迫我实现了这些方法,而且还在浪费我宝贵的工作时间来处理你的事件,特别是当我外出的时候更是如此!你能不能不再来烦我?”
于是,彼得意识到接口虽然在很多情况都很有用,但是当用作事件时,“粒度”不够好。他希望能够仅在别人想要时才通知他们,于是他决定把接口的方法分离为单独的委托,每个委托都像一个小的接口方法:
来看委托的定义
委托定义及基本知识
回调(callback)函数是Windows编程的一个重要部分。如果您具备C或C++编程背景,就曾在许多WindowsAPI中使用过回调。VB添加了AddressOf关键字后,开发人员就可以利用以前一度受到限制的API了。回调函数实际上是方法调用的指针,也称为函数指针,是一个非常强大的编程特性。.NET以委托的形式实现了函数指针的概念。它们的特殊之处是,与C函数指针不同,.NET委托是类型安全的。
基本上说当要把方法传送给其他方法时,需要使用委托。要了解它们的含义,可以看看下面的代码:inti = int.Parse("99");
我们习惯于把数据作为参数传递给方法,如上面的例子所示。所以,给方法传送另一个方法听起来有点奇怪。而有时某个方法执行的操作并不是针对数据进行的,而是要对另一个方法进行操作,这就比较复杂了。在编译时我们不知道第二个方法是什么,这个信息只能在运行时得到,所以需要把第二个方法作为参数传递给第一个方法。
委托定义 -----delegate string GetAString();
其语法类似于方法的定义,但没有方法体,定义的前面要加上关键字delegate。因为定义委托基本上是定义一个新类,所以可以在定义类的任何地方定义委托,既可以在另一个类的内部定义,也可以在任何类的外部定义,还可以在命名空间中把委托定义为顶层对象。根据定义的可见性,可以在委托定义上添加一般的访问修饰符:public、private和protected等:publicdelegate string GetAString(); 实际上,“定义一个委托”是指“定义一个新类”。委托实现为派生于基类System.MulticastDelegate的类,System.MulticastDelegate又派生于基类System.Delegate。
实例话委托-- GetAString firstStringMethod = new GetAString(x.ToString);
delegatevoid WorkStarted();
delegate void WorkProgressing();
delegate int WorkCompleted();
classWorker {
public void DoWork() {
Console.WriteLine(“工作:工作开始”);
if( started != null ) started();
Console.WriteLine(“工作: 工作进行中”);
if( progressing != null )progressing();
Console.WriteLine("“工作: 工作完成”");
if( completed != null ) {
int grade =completed();
Console.WriteLine(“工人的工作得分=” + grade);
}
}
public WorkStarted started;
public WorkProgressing progressing;
public WorkCompleted completed;
}
classBoss {
public int WorkCompleted() {
Console.WriteLine("Better...");
return 4; /* 总分为10 */
}
}
classCollage {
public void WorkProgressing () {
Console.WriteLine("busying.");
return 4; /* 总分为10 */
}
}
classUniverse {
static void Main() {
Worker peter = new Worker();
Boss boss = new Boss();
peter.completed = newWorkCompleted(boss.WorkCompleted);
peter.Progressing = new WorkProgressing (Collage. WorkProgressing);
peter.DoWork();
Console.WriteLine(“Main: 工人工作完成”);
Console.ReadLine();
}
}
4,静态监听者
这样,彼得不会再拿他老板不想要的事件来烦他老板了,但是他还没有把宇宙放到他的监听者列表中。因为宇宙是个包涵一切的实体,看来不适合使用实例方法的委托(想像一下,实例化一个“宇宙”要花费多少资源…..),于是彼得就需要能够对静态委托进行挂钩,委托对这一点支持得很好:
classUniverse {
static void WorkerStartedWork() {
Console.WriteLine("Universenotices worker starting work");
}
static int WorkerCompletedWork() {
Console.WriteLine("Universepleased with worker's work");
return 7;
}
static void Main() {
Worker peter = new Worker();
Boss boss = new Boss();
peter.completed = newWorkCompleted(boss.WorkCompleted);
peter.started = newWorkStarted(Universe.WorkerStartedWork);
peter.completed = newWorkCompleted(Universe.WorkerCompletedWork);
peter.DoWork();
Console.WriteLine(“Main: 工人工作完成”);
Console.ReadLine();
}
}
5.事件
不幸的是,宇宙太忙了,也不习惯时刻关注它里面的个体,它可以用自己的委托替换了彼得老板的委托。这是把彼得的Worker类的的委托字段做成public的一个无意识的副作用。同样,如果彼得的老板不耐烦了,也可以决定自己来激发彼得的委托if( peter.completed != null ) peter.completed();
彼得不想让这些事发生,他意识到需要给每个委托提供“注册”和“反注册”功能,这样监听者就可以自己添加和移除委托,但同时又不能清空整个列表也不能随意激发彼得的事件了。彼得并没有来自己实现这些功能,相反,他使用了event关键字让C#编译器为他构建这些方法:
classWorker {
...
public event WorkStarted started;
public event WorkProgressing progressing;
public event WorkCompleted completed;
}
彼得知道event关键字在委托的外边包装了一个property,仅让C#客户通过+=和-=操作符来添加和移除,强迫他的老板和宇宙正确地使用事件。
staticvoid Main() {
Worker peter = new Worker();
Boss boss = new Boss();
peter.completed += new WorkCompleted(boss.WorkCompleted);
peter.started += new WorkStarted(Universe.WorkerStartedWork);
peter.completed += newWorkCompleted(Universe.WorkerCompletedWork);
peter.DoWork();
Console.WriteLine(“Main: 工人工作完成”);
Console.ReadLine();
}
6.“收获”所有结果
到这时,彼得终于可以送一口气了,他成功地满足了所有监听者的需求,同时避免了与特定实现的紧耦合。但是他注意到他的老板和宇宙都为它的工作打了分,但是他仅仅接收了一个分数。面对多个监听者,他想要“收获”所有的结果,于是他深入到代理里面,轮询监听者列表,手工一个个调用:
publicvoid DoWork() {
...
Console.WriteLine("“工作: 工作完成”");
if( completed != null ) {
foreach( WorkCompleted wc incompleted.GetInvocationList() ) {
int grade =wc();
Console.WriteLine(“工人的工作得分=” + grade);
}
}
}
7.异步通知:A.激发& 忘掉
同时,他的老板和宇宙还要忙于处理其他事情,也就是说他们给彼得打分所花费的事件变得非常长:
classBoss {
public int WorkCompleted() {
System.Threading.Thread.Sleep(3000);
Console.WriteLine("Better...");return 6; /* 总分为10*/
}
}
classUniverse {
static int WorkerCompletedWork() {
System.Threading.Thread.Sleep(4000);
Console.WriteLine("Universe ispleased with worker's work");
return 7;
}
...
}
很不幸,彼得每次通知一个监听者后必须等待它给自己打分,现在这些通知花费了他太多的工作事件。于是他决定忘掉分数,仅仅异步激发事件:
publicvoid DoWork() {
...
Console.WriteLine("“工作: 工作完成”");
if( completed != null ) {
foreach( WorkCompleted wc incompleted.GetInvocationList() )
{
wc.BeginInvoke(null, null);
}
}
}
7.异步通知:B.轮询
这使得彼得可以通知他的监听者,然后立即返回工作,让进程的线程池来调用这些代理。随着时间的过去,彼得发现他丢失了他工作的反馈,他知道听取别人的赞扬和努力工作一样重要,于是他异步激发事件,但是周期性地轮询,取得可用的分数。
publicvoid DoWork() {
...
Console.WriteLine("“工作: 工作完成”");
if( completed != null ) {
foreach( WorkCompleted wc incompleted.GetInvocationList() ) {
IAsyncResultres = wc.BeginInvoke(null, null);
while(!res.IsCompleted )
{
Console.WriteLine(“doanthother job“);
System.Threading.Thread.Sleep(100);
}
int grade =wc.EndInvoke(res);
Console.WriteLine(“工人的工作得分=” + grade);
}
}
}
7.异步通知:C.等待
IAsyncResult.AsyncWaitHandle属性获取用于等待异步操作完成的WaitHandle
WaitHandle.WaitOne方法阻塞当前线程,直到当前的WaitHandle收到信号
使用WaitHandle,则在异步调用完成之后,但在通过调用EndInvoke结果之前,可以执行其他处理
publicvoid DoWork() {
...
Console.WriteLine("“工作: 工作完成”");
if (completed != null)
{
foreach (WorkCompleted wc incompleted.GetInvocationList())
{
IAsyncResult res =wc.BeginInvoke(null, null);
If(!res.AsyncWaitHandle.WaitOne(100,true));
{
Console.WriteLine(“doanthother job“);
}
int grade = wc.EndInvoke(res);
Console.WriteLine("worker'sgrade:" + grade);
}
}
}
7 异步通知:D.委托
不幸地,彼得有回到了一开始就想避免的情况中来,比如,老板站在背后盯着他工作。于是,他决定使用自己的委托作为他调用的异步委托完成的通知,让他自己立即回到工作,但是仍可以在别人给他的工作打分后得到通知:
public void DoWork() {
...
Console.WriteLine("“工作:工作完成”");
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(“工人的工作得分=”+ grade);
}
宇宙中的幸福
彼得、他的老板和宇宙最终都满足了。彼得的老板和宇宙可以收到他们感兴趣的事件通知,减少了实现的负担和非必需的往返“差旅费”。彼得可以通知他们,而不管他们要花多长时间来从目的方法中返回,同时又可以异步地得到他的结果。彼得知道,这并不*十分*简单,因为当他异步激发事件时,方法要在另外一个线程中执行,彼得的目的方法完成的通知也是一样的道理。
现在我们应该理解delegate的存在和它是如何帮助彼得的了。除了这个让我们再来看看其他的应用吧。
启动线程
在C#中,可以告诉计算机并行运行某些新的执行序列。这种序列就称为线程,在基类System.Threading.Thread的一个实例上使用方法Start(),就可以开始执行一个线程。如果要告诉计算机开始一个新的执行序列,就必须说明要在哪里执行该序列。必须为计算机提供开始执行的方法的细节,即Thread.Start()方法必须带有一个参数,该参数定义了要由线程调用的方法。
usingSystem;
usingSystem.Threading;
classTest
{
static void Main()
{
ThreadStart threadDelegate = newThreadStart(Work.DoWork);
Thread newThread = newThread(threadDelegate);
newThread.Start();
Work w = new Work();
w.Data = 42;
threadDelegate = newThreadStart(w.DoMoreWork);
newThread = new Thread(threadDelegate);
newThread.Start();
}
}
classWork
{
public static void DoWork()
{
Console.WriteLine("Static threadprocedure.");
}
public int Data;
public void DoMoreWork()
{
Console.WriteLine("Instance threadprocedure. Data={0}", Data);
}
}
策略模式(StrategyPattern)的delegate实现
Strategy模式是对算法的封装。即使是一个计算行为,如果其实现有其多样性,为达到易扩展的目的,我们可以将其抽象出来,以接口的形式来定义。利用了面向对象的多态性,在运行时,可以灵活的变更这个算法的具体实现。
例如编写一个类,它带有一个对象数组,并把它们按升序排列。但是,排序的部分过程会涉及到重复使用数组中的两个对象,比较它们,看看哪一个应放在前面。如果要编写的类必须能给任何对象数组排序,那么程这个类必须能够依据比较类型的不同采用不同的策略去比较。
另一个例子是以税收计算为例,假定税收策略分为个人所得税,和企业所得税。根据策略模式,将税收策略抽象为接口ITaxStrategy:
publicinterface ITaxStrategy
{
doubleCalculate(double income);
}
各种税收策略均实现该类。
publicclass PeronalTaxStrategy:ITaxStrategy
{
public double Calculate(double income)
{
//实现;
}
}
publicclass EnterpriseTaxStrategy:ITaxStrategy
{
public double Calculate(double income)
{
//实现;
}
}
如果此时有一个公共的类,提供税收的相关操作,其中就包括计算所得税的方法:
publicclass TaxOp
{
private ITaxStrategy strategy;
public TaxOp(ITaxStrategy strategy)
{
this.strategy = strategy;
}
public void ChangeStrategy(ITaxStrategy strategy)
{
this.strategy = strategy
}
public double GetTax(double income)
{
return strategy.Calculate(income);
}
}
客户端调用:
publicclass App
{
public static void Main(string[] args)
{
TaxOp op = new TaxOp(new PersonalTaxStrategy());
Console.WriteLine(“The Personal Tax is :{0}”,op.GetTax(1000));
}
}
为了达到上面的目的,我们也可以将原来抽象出来的接口修改为委托:
publicdelegate double CalculateTax(double income);
对于个人所得税和企业所得税的实现,相应修改为:
publicclass Tax
{
public static double CalculatePersonalTax(double income)
{
//实现;
}
public static double CalculateEnterpriseTax(double income)
{
//实现;
}
}
税收的公共类则修改如下:
publicclass TaxOp
{
private CalculateTax calDel;
public TaxOp(Calculate calDel)
{
this.calDel = calDel;
}
public double GetTax(double income)
{
return calDel(income);
}
}
客户端的调用:
publicclass App
{
public static void Main(string[] args)
{
TaxOp op = new TaxOp(new CalculateTax(Tax.CalculatePersonalTax));
Console.WriteLine(“The Personal Tax is :{0}”,op.GetTax(1000));
}
}
这两种方案的目的都是相同的,就是将算法和算法的使用者分开解耦,达到松耦合高聚合的目的。有过c/c++使用经验的人,可能将这两种方案的区别看成一个是是面向对象的,一个是面向过程的,前者(使用Interfaceclass 或者abstract class作为策略父类)是将方法封装在对象内部,而后者(使用Delegate)则直接对方法进行操作,同时又利用delegate委托来实现扩展,为什么呢,那是因为Delegate的使用方法和和c/c中的++函数指针是一样的,它的行为本质和函数指针一样。但是从OO来看,Delegate可以看成是对具有相同特性的行为(具备相同输入和输出)的一簇函数说进行的普遍意义上的抽象,从这个意义讲,委托是是面向对象的;从微软在c#2.0对委托的的输入和输出的拓展中,更可以看到委托OO的思想。
泛型委托 (2.0)
声明方式:
public delegate void Del<T>(Titem);
public static void Notify(int i) { }
Del<int> m1 = newDel<int>(Notify);
委托的逆变和协变 (2.0)
协变允许我们构建委托能指向返回类型及相关继承体系。
逆变允许构建委托指向多个方法,只要方法参数是存在继承关系的对象。
匿名方法(2.0)
到前为止,要想使委托工作,方法必须已经存在(即委托是用方法的签名定义的)。但使用委托还有另外一种方式:即通过匿名方法。匿名方法是用作委托参数的一个代码块。
用匿名方法定义委托的语法与前面的定义并没有什么区别。但在实例化委托时,就有区别了。下面是一个非常简单的控制台应用程序,说明了如何使用匿名方法:
namespaceConsoleApplication1
{
class Program
{
delegate string delegateTest(string val);
static void Main(string[] args)
{
string mid = ", middle part,";
delegateTest anonDel = delegate(string param)
{
param += mid;
param += " and this was added to the string.";
return param;
};
Console.WriteLine(anonDel("Startof string"));
}
}
}
委托delegateTest定义为一个类级变量,它带一个字符串参数。有区别的是Main方法。在定义anonDel时,不是传送已知的方法名,而是使用一个简单的代码块:
{
param += mid;
param += " and this was added to the string.";
return param;
};
可以看出,该代码块使用方法级的字符串变量mid,该变量是在匿名方法的外部定义的,并添加到要传送的参数中。接着代码返回该字符串值。在调用委托时,把一个字符串传送为参数,将返回的字符串输出到控制台上。
匿名方法的优点是减少了系统开销。方法仅在由委托使用时才定义。在为事件定义委托时,这是非常显然的。这有助于降低代码的复杂性,尤其是定义了好几个方法时,代码会显得比较简单。
在使用匿名方法时,必须遵循两个规则。在匿名方法中不能使用跳转语句跳到该匿名方法的外部,反之亦然:匿名方法外部的跳转语句不能跳到该匿名方法的内部。
在匿名方法内部不能访问不安全的代码。另外,也不能访问在匿名方法外部使用的ref和out参数。但可以使用在匿名方法外部定义的其他变量
Lambda运算符(3.0)和LINQ
Lambda 表达式”是一个匿名函数,它可以包含表达式和语句,并且可用于创建委托或表达式树类型(不再这里讨论)。适用于匿名方法的所有限制也适用于Lambda 表达式。
所有Lambda 表达式都使用 Lambda 运算符=>,该运算符读为“goesto”。该 Lambda 运算符的左边是输入参数(如果有),右边包含表达式或语句块。Lambda 表达式 x => x * x 读作“xgoes to x times x”。可以将此表达式分配给委托类型
delegateint del(int i);
staticvoid Main(string[] args)
{
del myDelegate = x => x * x;
int j = myDelegate(5); //j = 25
}
这些语句可以通过匿名函数表达为:
delegateint del(int i);
staticvoid Main(string[] args)
{
delmyDelegate = delegate(int x)
{
Return x*x;
}
int j = myDelegate(5); //j = 25
}
Lambda 在基于方法的LINQ 查询中用作标准查询运算符方法(如 Where)的参数
。
LINQ表达:
List<string>fruits =
new List<string> { "apple","passionfruit", "banana", "mango",
"orange", "blueberry", "grape","strawberry" };
IEnumerable<string>query = from f in fruits
where f. Length< 6 select f
foreach(string fruit in query)
{
Console.WriteLine(fruit);
}
标准lambda表达:
List<string> fruits =
new List<string> {"apple", "passionfruit", "banana","mango",
"orange","blueberry", "grape", "strawberry" };
IEnumerable<string> query =fruits.Where(fruit => fruit.Length < 6);
foreach (string fruit in query)
{
Console.WriteLine(fruit);
}