原文作者:
Shivprasad koirala
介绍
在这篇文章中, 我们会尝试着去理解delegate能解决什么样的问题, 然后会在实例中去使用。 之后, 我们要进一步理解多播委托的概念以及事件是如何封装委托的。 最终, 我们要明白事件和委托的不同, 学会如何异步调用委托。
在文章的最后,我们能能总结出委托的六种重要用处。
方法和函数的抽象问题
在讲委托之前,让我们先搞明白委托到底能解决什么问题。下面是一个很简单的类“ClsMaths”, 它只有一个方法“Add”。这个类会被一个简单的客户端消费(调用)。假设过了一段时间之后,现在客户端对ClsMaths这个类有了新的需求: 添加一个"Subtration"方法。那么,按之前的做法, 我们需要修改客户端已添加对新方法的调用代码。
换句话说, ClsMaths的一个新增方法导致了客户端的重新编译。
简单来说, 问题出现了: 功能类和消费类之间存在了紧耦合。所以如何解决?
我们可以选择使用委托作为中间件(垫片), 消费类不再是直接调用实现类的方法,而是调用一个虚拟指针(委托),让委托去调用真正的执行方法。这样,我们就把消费类和具体实现方法解耦了。
译者注, 他这里的ClsMaths类只有四个方法 加减乘除, 作者使用了一个委托变量来调用4个方法, 所以这里确实做到了解耦。
稍后你就可以看到因为抽象指针的作用,ClsMath的修改将不会对消费类产生任何影响。 这里的抽象指针就是委托啦。
/** 题外话,上图提到的Balsamiq Mockups是一个很棒的软件, 可以用来画UI效果图, 我喜欢用来画流程图(稍显不如visio方便, 但是阅读和美观效果完爆之) **/
如何创建一个委托
创建一个委托只要四步: 定义, 创建, 引用, 调用(和C# in depth 中的说法一致)
第一步是定义一个和函数有同样返回类型、输入参数的委托, 例如下面的Add函数有2个int类型输入参数以及一个int类型的输入参数。
1 private int Add(int i,int y) 2 { 3 return i + y; 4 }
对此, 我们可以定义如下的委托:
1 // Declare delegate 2 public delegate int PointetoAddFunction(int i,int y);
注意, 返回类型和输入类型要兼容, 否则会报错。
下一步就是创建一个委托类型的变量喽:
1 // Create delegate reference 2 PointetoAddFunction myptr = null;
最后就是调用了:
1 // Invoke the delegate 2 myptr.Invoke(20, 10)
下图为实例代码:
如何使用委托解决抽象指针问题
为了解耦算法的变化, 我们使用
一个抽象的指针指向所有的算法:(因为这四个方法的格式是一致的)
第一步, 在实现类中定义一个委托如下:(注意输入输出参数的格式)
1 public class clsMaths 2 { 3 public delegate int PointerMaths(int i, int y); 4 }
第二步, 定义一个返回委托的函数用以暴露具体实现方法给消费类:
1 public class clsMaths 2 { 3 public delegate int PointerMaths(int i, int y); 4 5 public PointerMaths getPointer(int intoperation) 6 { 7 PointerMaths objpointer = null; 8 if (intoperation == 1) 9 { 10 objpointer = Add; 11 } 12 else if (intoperation == 2) 13 { 14 objpointer = Sub; 15 } 16 else if (intoperation == 3) 17 { 18 objpointer = Multi; 19 } 20 else if (intoperation == 4) 21 { 22 objpointer = Div; 23 } 24 return objpointer; 25 } 26 }
下面就是完整的代码, 所有的具体实现函数都被标记为private, 只有委托和暴露委托的函数是public的。
1 public class clsMaths 2 { 3 public delegate int PointerMaths(int i, int y); 4 5 public PointerMaths getPointer(int intoperation) 6 { 7 PointerMaths objpointer = null; 8 if (intoperation == 1) 9 { 10 objpointer = Add; 11 } 12 else if (intoperation == 2) 13 { 14 objpointer = Sub; 15 } 16 else if (intoperation == 3) 17 { 18 objpointer = Multi; 19 } 20 else if (intoperation == 4) 21 { 22 objpointer = Div; 23 } 24 return objpointer; 25 } 26 27 private int Add(int i, int y) 28 { 29 return i + y; 30 } 31 private int Sub(int i, int y) 32 { 33 return i - y; 34 } 35 private int Multi(int i, int y) 36 { 37 return i * y; 38 } 39 private int Div(int i, int y) 40 { 41 return i / y; 42 } 43 }
所以消费类的调用就和具体实现方法没有耦合了:
1 int intResult = objMath.getPointer(intOPeration).Invoke(intNumber1,intNumber2);
多播委托
在我们之前的例子中,我们已经知道了如何创建委托变量和绑定具体实现方法到变量上。但实际上, 我们可以给一个委托附上若干个具体实现方法。如果我们调用这样的委托, 那么附到委托上的函数会顺序执行。(至于如果函数有返回值, 那么只有最后一个函数的返回值会被捕捉到)
1 // Associate method1 2 delegateptr += Method1; 3 // Associate Method2 4 delegateptr += Method2; 5 // Invoke the Method1 and Method2 sequentially 6 delegateptr.Invoke();
所以, 我们可以在“发布者/消费者”模式中使用多播委托。例如, 我们的应用中需要不同类型的错误日志处理方式,当错误发生时,我们需要把错误信息广播给不同的组件进行不同的处理。 (如下图)
多播委托的简单例子
我们可以通过下面这个例子更好的理解多播委托。 在这个窗体项目中,我们有“Form1”, “Form2”, “Form3”。
“Form1中有一个多播委托来把动作的影响传递到“Form2”和“Form3”中。
在"Form1"中, 我们首先定义一个委托以及委托变量, 这个委托是用来传递动作的影响到其他Form中的。
1 // Create a simple delegate 2 public delegate void CallEveryOne(); 3 4 // Create a reference to the delegate 5 public CallEveryOne ptrcall=null; 6 // Create objects of both forms 7 8 public Form2 obj= new Form2(); 9 public Form3 obj1= new Form3();
在“Form1”的Form_Load函数中, 我们调用其他的Forms;把其他表单中的CallMe方法附加到“Form1”的委托中。
1 private void Form1_Load(object sender, EventArgs e) 2 { 3 // Show both the forms 4 obj.Show(); 5 obj1.Show(); 6 // Attach the form methods where you will make call back 7 ptrcall += obj.CallMe; 8 ptrcall += obj1.CallMe; 9 }
最终, 我们在"Form1"的按钮点击函数中调用委托(多播的):
1 private void button1_Click(object sender, EventArgs e) 2 { 3 // Invoke the delegate 4 ptrcall.Invoke(); 5 }
多播委托的问题 -- 暴露过多的信息
上面例子的第一个问题就是, 消费者并没有权利来选择订阅或是不订阅,因为这个过程是由“Form1”也就是发布者来决定的。
我们可以用其他方式, 把委托传递给消费者, 让消费者来决定他们要不要订阅来自发布者(Form1)的多播委托。 但是, 这种做法会引发另个问题: 破坏封装。 如果我们把委托暴露给消费者, 就意味着委托完全裸露在了消费者面前。
事件 -- 委托的封装
事件能解决委托的封装问题。 事件包裹在委托之外, 使得消费者只能接收但不会有委托的完全控制权。
下图是对这一概念的图解:
1. 具体的实现方法被委托抽象和封装了
2. 委托被多播委托进一步封装了以提供广播的效果
3. 事件进一步封装了多播委托
实现事件
我们来把多播委托的例子改造成事件的方式。 第一步是在发布者“Form1”中定义委托和委托类型的事件; 下面就是对应的代码块,请注意关键字event。 我们定义了一个委托“CallEveryone”, 然后定义了一个委托类型的事件“EventCallEveryone”。
1 public delegate void CallEveryone(); 2 public event CallEveryone EventCallEveryOne;
从发布者“Form1”中创建“Form2”和“Form3”的对象, 然后把当前这个“Form1”对象传到“Form2”、 "Form3"中, 这样 2、 3就可以监听事件了。
1 Form2 obj = F new Form2(); 2 obj.obj = this; 3 Form3 obj1 = new Form3(); 4 obj1.obj = this; 5 obj.Show(); 6 obj1.Show(); 7 EventCallEveryOne();
在消费者这边, “Form2”和“Form3”自主决定是否把具体某个方法付到事件上。
1 obj.EventCallEveryOne += Callme;
这段代码的执行结果将会和我们上文的多播委托的例子结果一样。
委托和事件的不同
所以, 如果事件不是委托的语法糖那么他们之间的区别在哪? 我们在上文中已经提到了一个主要的区别: 事件比委托多了一层封装。因此, 如果我们传递委托, 那么消费者接受的是一个赤裸裸的委托, 用户可以修改委托的信息。 而我们使用事件,那么用户只能监听事件而不能修改它。
函数的异步委托调用
委托的另一种用法是异步函数的调用。 你能够异步的调用委托指向的函数。
异步调用意味着客户端调用委托之后, 代码的控制权又立即回到了客户端手中以继续执行后续的代码。
委托携带者调用者的信息在parallel的线程池中启用新的线程执行具体的函数, 当委托执行结束后, 它会发出信息通知客户端(调用者)。
为了能够异步得调用函数, 我们需要call “begininvoke”方法。 在“begininvoke”方法中, 我们需要为委托提供一个回调函数。 如下图的CallbackMethod。
1 delegateptr.BeginInvoke(new AsyncCallback(CallbackMethod), delegateptr);
下面的代码段就是一个回调函数的demo, 这段代码会在委托中的函数执行完成之后被立即调用。
1 static void CallbackMethod(IAsyncResult result) 2 { 3 int returnValue = flusher.EndInvoke(result); 4 }
总结委托的用法
委托有5种重要的使用方式:(译者注: 原文写的6种, 我只看到了5种)
1. 抽象、封装一个方法(匿名调用)
这是委托最重要的功能, 它帮助我们定义一个能够指向函数的抽象的指针。 同时, 这个指针还可以指向其他符合其规范的函数。
开头的时候我们展示的一个math类, 之后我们使用一个抽象指针就把该math类中添加的所有函数都包括在内了。 这个例子就很好的说明了委托的这一使用方法。
2. 回调机制
客户端可以使用委托来创建回调函数。
3. 异步执行
使用BeginInvoke 和 EndInvoke 我们可以异步得调用所有的委托。
4. 多点广播
有的时候, 我们希望能够让函数顺序执行, 那么多播委托就可以做到这一点。
5. 事件
事件可以帮助我们方便的建立 发布/订阅 模式。