欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

关于委托和事件的使用

程序员文章站 2022-10-18 13:04:30
原文:https://www.codeproject.com/articles/85296/important-uses-of-delegates-and-events 原文作者: Shivprasad koirala 介绍 在这篇文章中, 我们会尝试着去理解delegate能解决什么样的问题, 然 ......
原文: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类型的输入参数。
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. 事件
    事件可以帮助我们方便的建立 发布/订阅 模式。