C#委托及事件处理机制浅析

事件可以理解为某个对象所发出的消息,以通知特定动作(行为)的发生或状态的改变。行为的发生可能是来自用户交互,如鼠标点击;也可能源自其它的程序逻辑。在这里,触发事件的对象被称为事件(消息)发出者(sender),捕获和响应事件的对象被称作事件接收者。

在事件(消息)通讯中,负责事件发起的类对象并不知道哪个对象或方法会接收和处理(handle)这一事件。这就需要一个中介者(类似指针处理的方式),在事件发起者与接收者之间建立关联。在.NET
Framework中,定义了一个特殊的类型(delegate),来提供类似C++的函数指针的功能。本文通过一个定义和使用简单自定义事件的例子,对.NET的事件处理机制加以分析,以加深对事件处理原理的理解。

C#委托及事件处理机制浅析

如图所示,使用自定义事件,需要完成以下步骤:
   
1、声明(定义)一个委托类(型),或使用.NET程序集提供的委托类(型);
   
2、在一个类(事件定义和触发类,即事件发起者sender)中声明(定义)一个事件绑定到该委托,并定义一个用于触发自定义事件的方法;

3、在事件响应类(当然发起和响应者也可以是同一个类,不过一般不会这样处理)中定义与委托类型匹配的事件处理方法;
   
4、在主程序中订阅事件(创建委托实例,在事件发起者与响应者之间建立关联)。
   
5、在主程序中触发事件。

如按钮点击事件,就是用户在程序界面点击按钮控件时由按钮对象发出的消息,我们可以在界面程序中定义按钮点击事件处理方法来响应这一消息。这里就使用了委托处理机制。

一、委托的定义和使用

委托(委派)的声明(定义)格式如下所示:
    public
delegate void MyDelegateClass(string message);
   
其中delegate为委托类型关键字,MyDelegateClass是我们所定义的委托类的名称。委托类型类似C++的函数指针,而且是类型安全的函
数指针,如同C++的回调函数(CALLBACK)。委托(委派)类型有一个签名(或称识别标志,signature),只有与签名特征匹配的方法才可以
通过委托类型进行委派。

从上面的定义中,可以看出我们定义的MyDelegateClass类的签名特征,即只要是输入参数为string,返回类型为void的方法都可以通过
MyDelegateClass类进行指派。有了这一行定义语句,不需要我们再干什么,.NET编译环境就会自动为我们生成委托类
MyDelegateClass,并允许我们通过类似MyDelegateClass
delegateObj = new
MyDelegateClass(对象名.方法名)的方式创建委托实例,添加与该实例关联的方法引用。.NET是如何做的呢?
   
实际上,.NET在编译时,是根据我们的委托声明语句,为我们创建继承自System.MulticastDelegate(抽象类,其根类为
System.Delegate)的委托类。Delegate类具有Target和Method两个类似指针的(引用)属性,分别指向所引用的对象及其方
法的地址,这样,我们在使用委托类实例时实际上就是在调用对应的对象方法。而且,Delegate类可以引用多个对象方法,利用其“+=”操作符,通过类
似delegateObj
+= new
MyDelegateClass(对象名.方法名)的语句,可以为委托类对象实例delegateObj添加多个方法引用,这些方法引用被保存在委托类的
委托列表中,在使用委托类实例时,这些方法都会被调用。

C#委托及事件处理机制浅析

如果需要,我们可以通过Delegate类的GetInvocationList()取出这些委托,并查看其Target和Method属性,获取所引用的方法名等信息。

下面以一个简单例子来演示一下委托类型的定义和使用。

1、创建一个目标类极其方法,提供给委托类型使用。

//TargetClass.cs

using
System;
    using
System.Collections.Generic;
    using
System.Text;

namespace
Delegatete_EventTest
    {
       
//也可以创建单独的类文件
       
public class TargetClass
       
{
           
public static void Method1(string message1)
           
{
               
Console.WriteLine("调用了目标方法1,参数:" + message1);
           
}
           
public void Method2(string message2)
           
{
               
Console.WriteLine("调用了目标方法2,参数:" + message2);
           
}
       
}
    }

2、在主程序中定义并使用委托类型。如图所示为程序中定义的委托类(包括其基类)的类视图:

C#委托及事件处理机制浅析

//DeleGateExample.cs

using
System;
    using
System.Collections.Generic;
    using
System.Text;

namespace
Delegatete_EventTest
    {
       
class DeleGateExample
       
{
           
//定义委托类型

public
delegate void MyDelegateClass(string message);

//主程序方法

static
void Main(string[] args)
           
{
               
//Test1();
               
//Test2();
              
 Test3();
            
}
            
//测试1(仅为委托实例指派了一个目标方法)

static void Test1()
            
{
                 //定义委托实例,并指派(关联)目标方法(注意是目标类的静态方法)

MyDelegateClass
delegateObj = new MyDelegateClass(TargetClass.Method1);

//运行委托实例(调用目标方法)

delegateObj("just a test");

//显示委托实例所关联的目标类极其方法

Console.WriteLine("目标对象及方法:" + delegateObj.Target + ","

+ delegateObj.Method);

}

//测试2(如果不通过+=操作符而指派第二个目标方法,会覆盖掉第一个目标方法关联)
            
static void Test2()
            
{

//定义委托类对象实例,并指派第一个目标方法(目标类静态方法)

MyDelegateClass delegateObj = new
MyDelegateClass(TargetClass.Method1);

//为委托实例指派第二个目标方法(目标类对象方法)

TargetClass targetobj = new TargetClass();
                
delegateObj = new MyDelegateClass(targetobj.Method2);

//运行委托实例(调用目标方法)

delegateObj("just a test");

//显示委托列表包含的目标方法个数

Console.WriteLine("该委托实例的目标方法个数:"

+ delegateObj.GetInvocationList().Length);

//显示委托实例的目标类极其方法名称

Console.WriteLine("目标对象及方法:" + delegateObj.Target + ","

+ delegateObj.Method);

}

//测试3(委托调用及委托列表显示)
            
static void Test3()
            
{

//定义委托对象实例,并关联第一个目标方法(目标类的静态方法)
                
MyDelegateClass delegateObj = new
MyDelegateClass(TargetClass.Method1);

//使用+=操作符为委托实例添加第二个目标方法(目标类对象方法)
                
TargetClass targetobj = new TargetClass();
                
delegateObj += new MyDelegateClass(targetobj.Method2);
                
//运行委托实例(调用目标方法)

//delegateObj.Invoke("just a tets");
                
delegateObj("just a tets");

//调用委托列表显示方法

DisplayDeObjList(delegateObj);
            
}

//委托列表的显示方法(逐一显示委托列表所包含的目标类极其方法名称)

static
void DisplayDeObjList(MyDelegateClass delegateObj)
             
{
                  //显示委托列表包含的目标方法个数

Console.WriteLine("该委托实例的目标方法列表中存在" +

delegateObj.GetInvocationList().Length+"个目标方法,分别是:");

//逐一显示委托列表中所指派的目标类极其方法名称

for (int i = 0; i <
delegateObj.GetInvocationList().Length; i++)
                 
{
                     
MyDelegateClass deObj =
(MyDelegateClass)delegateObj.GetInvocationList()[i];
                     
Console.WriteLine("目标对象及方法:" + deObj.Target + "," +
deObj.Method);
                 
}
             
}

}

}
   
二、自定义事件的定义与处理

1、在事件发起者类中定义事件:

//EventSenderClass.cs

using
System;
    using
System.Collections.Generic;
    using
System.Text;

namespace
Self_DefinedEvent
    {
       
//声明一个委托类(定义为公共类型,以便外部代码使用)
       
public delegate void MyEventDelegate(string aMessage);//参数为提示信息

class EventSenderClass
       
{
           
//定义一个事件属性
           
public event MyEventDelegate selfEvent;
           
//定义一个激发自定义事件的方法
           
public void RaiseSelfDefinedEvent()
           
{
               
//事件是否被订阅(被实例化),如果未订阅,MessageArrived就是null,不会引发事件
               
if (selfEvent != null)
                   
selfEvent("Self-Defined event is raised.");
           
}
      
 }
    }

2、在事件接收与处理类中定义事件处理方法

//EventHandlerClass.cs

using
System;
    using
System.Collections.Generic;
    using
System.Text;

namespace
Self_DefinedEvent
    {
       
public class EventHandlerClass
       
{
           
//定义接收消息的公共属性
           
public string receivedMessage;
           
//自定义事件的处理方法

public void ReceiveAndDisplayMessage(string message)
           
{
               
receivedMessage = "自定义事件被响应,事件消息为:" + message;
           
}
      
 }
    }

3、本例基于窗口应用,把窗口(Form)类作为自定义事件处理的主程序。在初始化窗口对象时执行自定义事件的订阅,即为自定义事件添加负责事件接收和处
理的对象方法(语法与前面例子中添加委托实例的目标方法相同);在窗口类中添加了一个按钮和一个标签控件,并把自定义事件的触发放在了按钮点击处理方法
中。点击按钮,自定义事件被触发,并使用标签控件输出事件响应信息。

C#委托及事件处理机制浅析

//Form1.cs

using
System;
    ......

using
System.Windows.Forms;

namespace
Self_DefinedEvent
    {
       
public partial class Form1 : Form
       
{
           
EventSenderClass myEventSender;
          
 EventHandlerClass myEventHandler;

public Form1()
           
{
               
InitializeComponent();
               
myEventSender = new EventSenderClass();
               
myEventHandler = new EventHandlerClass();
               
//订阅(实例化)自定义事件

myEventSender.selfEvent +=

new MyEventDelegate(myEventHandler.ReceiveAndDisplayMessage);
           
}

//按钮点击处理方法

private void button1_Click(object sender, EventArgs e)
           
{
               
//触发自定义事件
               
myEventSender.RaiseSelfDefinedEvent();
               
label1.Text = label1.Text + myEventHandler.receivedMessage;
          
 }
       
}
    }
   
4、以按钮为例,理解.NET的事件处理方式

实际上,.NET的控件事件处理方式正是采用了前面所讲的自定义事件的处理机制。以上例中的按钮事件处理为例,打开Form1.Designer.cs,可以找到按钮事件的订阅语句:

this.button1.Click += new
System.EventHandler(this.button1_Click);

解析一下这个语句,“Click”是System.Windows.Forms.Button按钮类的事件属性,button1_Click是处理按钮事
件的目标方法名,System.EventHandler则是.NET已定义好的用于事件处理的委托类型。这是.NET事件订阅的典型语法。

5、动态控件的定义和使用

在实际项目中有时事先并不知道程序界面中需要哪些控件,需要几个,这时就需要根据不同的条件动态生成不同的控件并使用。这里我们仅以一个简单例子加以说明。

在上面的Windows界面应用程序中添加一个界面类Form2.cs:

......

namespace
Self_DefinedEvent
    {
       
public partial class Form2 : Form
       
{
           
public Form2()
           
{
               
InitializeComponent();
               
Button but1 = new Button();
               
but1.Text = "动态按钮";
               
but1.Click += new EventHandler(this.but1_Click);
               
this.Controls.Add(but1);
           
}
          
 //动态按钮处理方法
           
private void but1_Click(object sender, EventArgs e)
           
{
               
Label lb = new Label();
               
//设置标签位置,实际应用中要涉及到界面布局,如利用动态表格设置控件位置等。
               
lb.Location = new System.Drawing.Point(0, 30);
               
lb.Size = new System.Drawing.Size(200,10);
               
this.Controls.Add(lb);
               
lb.Text = "Button is clicked.";
          
 }
       
}
    }

修改项目中Program.cs中的内容,将加载Form1界面的语句改成加载Form2界面,试一下动态按钮的使用:

//Program.cs

......

Application.Run(new Form2());

......

三、用delegate实现回调函数(类似C++的CALLBACK)

1、C++的回调函数实现原理
   
C++的回调函数实现原理是子程序(子类)调用主程序(主类,注意这里的主类和子类为相互独立的类,并不存在继承关系)的函数(方法)。这里以WinCE
的UDP通讯为例讲解。其原理为为:UDP通讯程序包括一个对话框主类CUDPDemoDlg,和包含打开本地端口、发送数据、接收数据等方法的子类
CUDP_CE。其中子类的接收数据方法运行在一个独立的线程中,以循环的方式读取远端发送来的数据,当接收到数据后,通过回调函数将接受的数据传递给主
程序。具体实现过程为:

(1)在子程序中定义回调函数类型,如:
    
typedef void (CALLBACK* ONUDPRECV)(void*,char* buf,DWORD
dwBufLen,sockaddr* saRecvAddress);
    
其中的void*参数一般对应主程序地址(主类对象指针)。
   
(2)在子类中定义回调函数类型的实例,如:
    
public ONUDPRECV  m_OnUdpRecv;
    
注意访问类型要设置为public,便于主程序访问。可以在子类构造函数中将回调函数类型实例初始化为null(m_OnUdpRecv =
null)。
   
(3)在主类中定义一个回调函数(注意函数的输入和返回参数类型与子程序中定义的回调函数类型是一致的,这点与委托类型相似),如:

private static void CALLBACK OnUdpCERecv(void * pOwner,char*
buf,DWORD dwBufLen,sockaddr * addr);
   
(4)在主类(对话框)的“打开”连接按钮方法中,设置子类对象的回调函数类型实例为上面定义的回调函数,同时调用子类对象的打开连接函数(注意将主类对象引用--主程序指针传递给了子类):

m_CEUdp.m_OnUdpRecv = OnUdpCERecv;
    
DWORD nResult =
m_CEUdp.Open(this,m_LocalPort,......,m_RemotePort);
   
(5)在子类的open方法CUDP_CE::Open(void* pOwner,int localPort,LPCTSTR
remoteHost,int remotePort)中启动数据接收线程,运行数据接收方法。
    
//传递主类对象指针,m_pOwner为子类中定义的保存对象指针的属性:

void
* m_pOwner;

m_pOwner = pOwner;
    
//创建线程并运行数据接收方法,CUDP_CE::RecvThread(......)为子类的数据接收方法
    
AfxBeginThread(RecvThread,this);
   
(6)在子类的数据接收方法中,当接收到数据时通过回调函数将数据回传给主类对象:
    UINT
CUDP_CE::RecvThread(LPVOID lparam)
    {
         
CUDP_CE *pSocket =
(CUDP_CE*)lparam;

......
         
while (TRUE)
        
{
              
......
              
//调用回调函数将数据发送出去
              
if (pSocket->m_OnUdpRecv)
              
{
                  
pSocket->m_OnUdpRecv(pSocket->m_pOwner,pSocket->m_recvBuf,......);

}
              
......
       
 }
       
 ......
    }
   
(7)在主类的回调函数中处理并显示通过UDP连接从远端接收到的数据。
   
//UDP数据接收回调函数
    void
CALLBACK CUDPDemoDlg::OnUdpCERecv(void * pOwner,char* buf,DWORD
dwBufLen,......)
    {
        
BYTE *pRecvBuf = NULL; //接收缓冲区
        
//得到父对象指针
         CUDPDemoDlg*
pThis = (CUDPDemoDlg*)pOwner;
        
//将接收的缓冲区拷贝到pRecvBuf中
        
pRecvBuf = new BYTE[dwBufLen];
        
CopyMemory(pRecvBuf,buf,dwBufLen);

//发送异步消息,表示收到串口数据。在主类中定义了WM_RECV_UDP_DATA自定义消息,及消息处理函

//数,消息处理函数负责将接收到的数据在文本控件中显示,过程略......
        
pThis->PostMessage(WM_RECV_UDP_DATA,WPARAM(pRecvBuf),dwBufLen);

}

2、在C#中用delegate实现回调函数

在C#中只需把主程序(主类)的方法作为委托的目标方法,就可以很容易地实现类似CALLBACK的回调函数,这里不再赘述。有兴趣的读者可以自行实现。

参考教材

C#入门经典(第三版)

Windows+CE+嵌入式高级编程及其实例详解(用C++实现)

网上参考文章

Handling and Raising Events

Delegate比较全面的例子

用户控件事件使用delegate

在C#中使用代理的方式触发事件

详解C#委托,事件与回调函数

Windows窗体间的消息传递

上一篇:使用C#三维图形控件进行曲线曲面分析


下一篇:Kafka如何保证消息不丢失不重复