关于委托和事件的使用

原文:https://www.codeproject.com/articles/85296/important-uses-of-delegates-and-events

原文作者:Shivprasad koirala

介绍

在这篇文章中, 我们会尝试着去理解delegate能解决什么样的问题, 然后会在实例中去使用。 之后, 我们要进一步理解多播委托的概念以及事件是如何封装委托的。 最终, 我们要明白事件和委托的不同, 学会如何异步调用委托。

在文章的最后,我们能能总结出委托的六种重要用处。

方法和函数的抽象问题

在讲委托之前,让我们先搞明白委托到底能解决什么问题。下面是一个很简单的类“ClsMaths”, 它只有一个方法“Add”。这个类会被一个简单的客户端消费(调用)。假设过了一段时间之后,现在客户端对ClsMaths这个类有了新的需求: 添加一个"Subtration"方法。那么,按之前的做法, 我们需要修改客户端已添加对新方法的调用代码。

换句话说, ClsMaths的一个新增方法导致了客户端的重新编译。

关于委托和事件的使用

简单来说, 问题出现了: 功能类和消费类之间存在了紧耦合。所以如何解决?

我们可以选择使用委托作为中间件(垫片), 消费类不再是直接调用实现类的方法,而是调用一个虚拟指针(委托),让委托去调用真正的执行方法。这样,我们就把消费类和具体实现方法解耦了。

译者注, 他这里的ClsMaths类只有四个方法 加减乘除, 作者使用了一个委托变量来调用4个方法, 所以这里确实做到了解耦。

稍后你就可以看到因为抽象指针的作用,ClsMath的修改将不会对消费类产生任何影响。 这里的抽象指针就是委托啦。

关于委托和事件的使用

/** 题外话,上图提到的Balsamiq Mockups是一个很棒的软件, 可以用来画UI效果图, 我喜欢用来画流程图(稍显不如visio方便, 但是阅读和美观效果完爆之) **/

如何创建一个委托

创建一个委托只要四步: 定义, 创建, 引用, 调用(和C# in depth 中的说法一致)

关于委托和事件的使用

第一步是定义一个和函数有同样返回类型、输入参数的委托, 例如下面的Add函数有2个int类型输入参数以及一个int类型的输入参数。

private int Add(int i,int y)
 {
     return i + y;
 }

对此, 我们可以定义如下的委托:

// Declare delegate
 public delegate int PointetoAddFunction(int i,int y);
注意, 返回类型和输入类型要兼容, 否则会报错。关于委托和事件的使用

下一步就是创建一个委托类型的变量喽:

// Create delegate reference
 PointetoAddFunction myptr = null;

最后就是调用了:

1 // Invoke the delegate
2 myptr.Invoke(20, 10)

下图为实例代码:

关于委托和事件的使用

如何使用委托解决抽象指针问题

为了解耦算法的变化, 我们使用一个抽象的指针指向所有的算法:(因为这四个方法的格式是一致的)

关于委托和事件的使用

第一步, 在实现类中定义一个委托如下:(注意输入输出参数的格式)

public class clsMaths
 {
   public delegate int PointerMaths(int i, int y);
 }

第二步, 定义一个返回委托的函数用以暴露具体实现方法给消费类:

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 }

所以消费类的调用就和具体实现方法没有耦合了:

int intResult = objMath.getPointer(intOPeration).Invoke(intNumber1,intNumber2);

多播委托

在我们之前的例子中,我们已经知道了如何创建委托变量和绑定具体实现方法到变量上。但实际上, 我们可以给一个委托附上若干个具体实现方法。如果我们调用这样的委托, 那么附到委托上的函数会顺序执行。(至于如果函数有返回值, 那么只有最后一个函数的返回值会被捕捉到)

// Associate method1
 delegateptr += Method1;
 // Associate Method2
 delegateptr += Method2;
 // Invoke the Method1 and Method2 sequentially
 delegateptr.Invoke();

所以, 我们可以在“发布者/消费者”模式中使用多播委托。例如, 我们的应用中需要不同类型的错误日志处理方式,当错误发生时,我们需要把错误信息广播给不同的组件进行不同的处理。 (如下图)

关于委托和事件的使用

多播委托的简单例子

我们可以通过下面这个例子更好的理解多播委托。 在这个窗体项目中,我们有“Form1”,“Form2”,“Form3”。

“Form1中有一个多播委托来把动作的影响传递到“Form2”和“Form3”中。

关于委托和事件的使用

在"Form1"中, 我们首先定义一个委托以及委托变量, 这个委托是用来传递动作的影响到其他Form中的。

// Create a simple delegate
 public delegate void CallEveryOne();
 
 // Create a reference to the delegate
 public CallEveryOne ptrcall=null;
 // Create objects of both forms
 
 public Form2 obj= new Form2();
 public Form3 obj1= new Form3();

在“Form1”的Form_Load函数中, 我们调用其他的Forms;把其他表单中的CallMe方法附加到“Form1”的委托中。

private void Form1_Load(object sender, EventArgs e)
 {
   // Show both the forms
   obj.Show();
   obj1.Show();
   // Attach the form methods where you will make call back
   ptrcall += obj.CallMe;
   ptrcall += obj1.CallMe;
 }
最终, 我们在"Form1"的按钮点击函数中调用委托(多播的):
private void button1_Click(object sender, EventArgs e)
 {
   // Invoke the delegate
4   ptrcall.Invoke();
 }

多播委托的问题 -- 暴露过多的信息

上面例子的第一个问题就是, 消费者并没有权利来选择订阅或是不订阅,因为这个过程是由“Form1”也就是发布者来决定的。

我们可以用其他方式, 把委托传递给消费者, 让消费者来决定他们要不要订阅来自发布者(Form1)的多播委托。 但是, 这种做法会引发另个问题: 破坏封装。 如果我们把委托暴露给消费者, 就意味着委托完全裸露在了消费者面前。

关于委托和事件的使用

事件 -- 委托的封装

事件能解决委托的封装问题。 事件包裹在委托之外, 使得消费者只能接收但不会有委托的完全控制权。

下图是对这一概念的图解:

1. 具体的实现方法被委托抽象和封装了

2. 委托被多播委托进一步封装了以提供广播的效果

3. 事件进一步封装了多播委托

关于委托和事件的使用

实现事件

我们来把多播委托的例子改造成事件的方式。 第一步是在发布者“Form1”中定义委托和委托类型的事件; 下面就是对应的代码块,请注意关键字event。 我们定义了一个委托“CallEveryone”, 然后定义了一个委托类型的事件“EventCallEveryone”。

public delegate void CallEveryone();
 public event CallEveryone EventCallEveryOne;

从发布者“Form1”中创建“Form2”和“Form3”的对象, 然后把当前这个“Form1”对象传到“Form2”、 "Form3"中, 这样 2、 3就可以监听事件了。

Form2 obj = F new Form2();
 obj.obj = this;
 Form3 obj1 = new Form3();
 obj1.obj = this;
 obj.Show();
 obj1.Show();
 EventCallEveryOne();

在消费者这边, “Form2”和“Form3”自主决定是否把具体某个方法付到事件上。

obj.EventCallEveryOne += Callme;

这段代码的执行结果将会和我们上文的多播委托的例子结果一样。

委托和事件的不同

所以, 如果事件不是委托的语法糖那么他们之间的区别在哪? 我们在上文中已经提到了一个主要的区别: 事件比委托多了一层封装。因此, 如果我们传递委托, 那么消费者接受的是一个赤裸裸的委托, 用户可以修改委托的信息。 而我们使用事件,那么用户只能监听事件而不能修改它。

函数的异步委托调用

委托的另一种用法是异步函数的调用。 你能够异步的调用委托指向的函数。

异步调用意味着客户端调用委托之后, 代码的控制权又立即回到了客户端手中以继续执行后续的代码。

委托携带者调用者的信息在parallel的线程池中启用新的线程执行具体的函数, 当委托执行结束后, 它会发出信息通知客户端(调用者)。

为了能够异步得调用函数, 我们需要call “begininvoke”方法。 在“begininvoke”方法中, 我们需要为委托提供一个回调函数。 如下图的CallbackMethod。

delegateptr.BeginInvoke(new AsyncCallback(CallbackMethod), delegateptr);

下面的代码段就是一个回调函数的demo, 这段代码会在委托中的函数执行完成之后被立即调用。

static void CallbackMethod(IAsyncResult result)
 {
   int returnValue = flusher.EndInvoke(result);
 }

总结委托的用法

委托有5种重要的使用方式:(译者注: 原文写的6种, 我只看到了5种)

1. 抽象、封装一个方法(匿名调用)

这是委托最重要的功能, 它帮助我们定义一个能够指向函数的抽象的指针。 同时, 这个指针还可以指向其他符合其规范的函数。

开头的时候我们展示的一个math类, 之后我们使用一个抽象指针就把该math类中添加的所有函数都包括在内了。 这个例子就很好的说明了委托的这一使用方法。

2. 回调机制

客户端可以使用委托来穿件回调函数。

3. 异步执行

使用BeginInvoke 和 EndInvoke 我们可以异步得调用所有的委托。

4. 多点广播

有的时候, 我们希望能够让函数顺序执行, 那么多播委托就可以做到这一点。

5. 事件

事件可以帮助我们方便的建立 发布/订阅 模式。

h1

相关推荐