委托:委托是一个类型,是一个函数指针类型,这个类型将该委托的实例化对象所能指向的函数的细节封装起来了,既规定了所能指向的函数的签名,也就是限制了所能指向的函数的参数和返回值。当实例化委托的时候,委托对象会指向某一个匹配的函数,实质就是将函数的地址赋值给了该委托的对象,然后就可以通过该委托对象来调用所指向的函数了。
多播委托:委托也可以包含多个方法,这时候要向委托对象中添加多个方法,这种委托称为多播委托。
多播委托的每一个方法都要与委托所限定的方法的返回值、参数匹配。
INvoke()方法是委托的同步调用的方法。
注意:不管是多播委托还是单播委托,在没有特殊处理的情况下,在一个线程的执行过程中去调用委托,调用委托的执行是不会新起线程的,这个执行还是在原线程中的,这个对于时间也是一样。当然,如果是在委托所指向的函数里面去启动一个新的线程那就是另外一件事了。
事件:
触发事件后,事件所指向的函数将会被执行。这种执行是通过事件名称来调用的,就想委托对象名一样的。
触发事件的方法只能在A类中定义,事件的实例化,以及实例化之后的实现体都只能在A类外定义。
在B类中去调用A类中的触发事件的方法:用A类的对象去调用A类的触发事件的方法。
程序实例
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
using System.Windows.Forms;
namespace DelegateStudy
{
public delegate void DelegateClick (int a);
public class butt
{
public event DelegateClick Click;
public void OnClick(int a)
{
if(Click != null)
Click.Invoke(a);
//Click(a);//这种方式也是可以的
MessageBox.Show("Click();");
}
}
class Frm
{
public static void Btn_Click(int a)
{
for (long i = 0; i < a; i++)
Console.WriteLine(i.ToString());
}
static void Main(string[] args)
{
butt b = new butt();
//在委托中,委托对象如果是null的,直接使用+=符号,会报错,但是在事件中,初始化的时候,只能用+=
b.Click += new DelegateClick (Fm_Click); //事件是基于委托的,所以委托推断一样适用,下面的语句一样有效:b.Click += Fm_Click;
//b.Click(10);错误:事件“DelegateStudy.butt.Click”只能出现在 += 或 -= 的左边(从类型“DelegateStudy.butt”中使用时除外)
b.OnClick (10000);
MessageBox.Show("sd234234234");
Console.ReadLine();
}
}
}
控件事件
基于Windows的应用程序也是基于消息的。这说明,应用程序是通过Windows来与用户通信的,Windows又是使用预定义的消息与应用程序通信的。这些消息是包含各种信息的结构,应用程序和Windows使用这些信息决定下一步的操作。
比如:当用户用鼠标去点击一个windows应用程序的按钮的时候,windows操作系统就会捕获到这个点击按钮的动作,这个时候它会根据捕获到的动作发送一个与之对应的预定义的消息给windows应用程序的这个按钮,windows应用程序的按钮消息处理程序会处理接收到的消息,这个程序处理过程就是根据收到的消息去触发相应的事件,事件被按钮触发后,会通知所有的该事件的订阅者来接收这个事件,从而执行相应的的函数。
在MFC等库或VB等开发环境推出之前,开发人员必须处理Windows发送给应用程序的消息。VB和今天的.NET把这些传送来的消息封装在事件中。如果需要响应某个消息,就应处理对应的事件。
控件事件委托EventHandler
在控件事件中,有很多的委托,在这里介绍一个最常用的委托EventHandler,.NET Framework中控件的事件很多都基于该委托,EventHandler委托已在.NET Framework中定义了。它位于System命名空间:
Public delegate void EventHandler(object sender,EventArgs e);
委托EventHandler参数和返回值
事件最终会指向一个或者多个函数,函数要与事件所基于的委托匹配。事件所指向的函数(事件处理程序)的命名规则:按照约定,事件处理程序应遵循“object_event”的命名约定。object就是引发事件的对象,而event就是被引发的事件。从可读性来看,应遵循这个命名约定。
首先,事件处理程序总是返回void,事件处理程序不能有返回值。其次是参数,只要是基于EventHandler委托的事件,事件处理程序的参数就应是object和EventArgs类型:
第一个参数接收引发事件的对象,比如当点击某个按钮的时候,这个按钮要触发单击事件最终执行这个函数,那么就会把当前按钮传给sender,当有多个按钮的单击事件都指向这个函数的时候,sender的值就取决于当前被单击的那个按钮,所以可以为几个按钮定义一个按钮单击处理程序,接着根据sender参数确定单击了哪个按钮:
if(((Button)sender).Name ==”buttonOne”)
第二个参数e是包含有关事件的其他有用信息的对象。
控件事件的其他委托
控件事件还有其他的委托,比如在窗体上有与鼠标事件关联的委托:
Public delegate void MouseEventHandler(object sender,MouseEventArgs e);
public event MouseEventHandler MouseDown;
this.MouseDown += new System.Windows.Forms.MouseEventHandler(this.Form1_MouseDown);
private void Form1_MouseDown(object sender, MouseEventArgs e){};
MouseDown事件使用MouseDownEventArgs,它包含鼠标的指针在窗体上的的X和Y坐标,以及与事件相关的其他信息。
控件事件中,一般第一个参数都是object sender,第二个参数可以是任意类型,不同的委托可以有不同的参数,只要它派生于EventArgs即可。
14.2.2.4、程序实例
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
namespace SecondChangeEvent1
{
// 该类用来存储关于事件的有效信息外,
// 还用来存储额外的需要传给订阅者的Clock状态信息
public class TimeInfoEventArgs : EventArgs
{
public TimeInfoEventArgs(int hour,int minute,int second)
{
this.hour = hour;
this.minute = minute;
this.second = second;
}
public readonly int hour;
public readonly int minute;
public readonly int second;
}
// 定义名为SecondChangeHandler的委托,封装不返回值的方法,
// 该方法带参数,一个clock类型对象参数,一个TimeInfoEventArgs类型对象
public delegate void SecondChangeHandler(
object clock,
TimeInfoEventArgs timeInformation
);
// 被其他类观察的钟(Clock)类,该类发布一个事件:SecondChange。观察该类的类订阅了该事件。
public class Clock
{
// 代表小时,分钟,秒的私有变量
int _hour;
public int Hour
{
get { return _hour; }
set { _hour = value; }
}
private int _minute;
public int Minute
{
get { return _minute; }
set { _minute = value; }
}
private int _second;
public int Second
{
get { return _second; }
set { _second = value; }
}
// 要发布的事件
public event SecondChangeHandler SecondChange;
// 触发事件的方法
protected void OnSecondChange(
object clock,
TimeInfoEventArgs timeInformation
)
{
// Check if there are any Subscribers
if (SecondChange != null)
{
// Call the Event
SecondChange(clock, timeInformation);
}
}
// 让钟(Clock)跑起来,每隔一秒钟触发一次事件
public void Run()
{
for (; ; )
{
// 让线程Sleep一秒钟
Thread.Sleep(1000);
// 获取当前时间
System.DateTime dt = System.DateTime.Now;
// 如果秒钟变化了通知订阅者
if (dt.Second != _second)
{
// 创造TimeInfoEventArgs类型对象,传给订阅者
TimeInfoEventArgs timeInformation =
new TimeInfoEventArgs(
dt.Hour, dt.Minute, dt.Second);
// 通知订阅者
OnSecondChange(this, timeInformation);
}
// 更新状态信息
_second = dt.Second;
_minute = dt.Minute;
_hour = dt.Hour;
}
}
}
/* ======================= Event Subscribers =============================== */
// 一个订阅者。DisplayClock订阅了clock类的事件。它的工作是显示当前时间。
public class DisplayClock
{
// 传入一个clock对象,订阅其SecondChangeHandler事件
public void Subscribe(Clock theClock)
{
theClock.SecondChange +=
new SecondChangeHandler(TimeHasChanged);
}
// 实现了委托匹配类型的方法
public void TimeHasChanged(
object theClock, TimeInfoEventArgs ti)
{
Console.WriteLine("Current Time: {0}:{1}:{2}",
ti.hour.ToString(),
ti.minute.ToString(),
ti.second.ToString());
}
}
// 第二个订阅者,他的工作是把当前时间写入一个文件
public class LogClock
{
public void Subscribe(Clock theClock)
{
theClock.SecondChange +=
new SecondChangeHandler(WriteLogEntry);
}
// 这个方法本来应该是把信息写入一个文件中
// 这里我们用把信息输出控制台代替
public void WriteLogEntry(
object theClock, TimeInfoEventArgs ti)
{
Clock a = (Clock)theClock;
Console.WriteLine("Logging to file: {0}:{1}:{2}",
a.Hour.ToString(),
a.Minute.ToString(),
a.Second.ToString());
}
}
/* ======================= Test Application =============================== */
// 测试拥有程序
public class Test
{
public static void Main()
{
// 创建clock实例
Clock theClock = new Clock();
// 创建一个DisplayClock实例,让其订阅上面创建的clock的事件
DisplayClock dc = new DisplayClock();
dc.Subscribe(theClock);
// 创建一个LogClock实例,让其订阅上面创建的clock的事件
LogClock lc = new LogClock();
lc.Subscribe(theClock);
// 让钟跑起来
theClock.Run();
}
}
}
- 3、小结
(1)、在定义事件的那个类A里面,可以任意的使用事件名,可以触发;在别的类里面,事件名只能出现在 += 或 -= 的左边来指向函数,即只能实例化,不能直接用事件名触发。但是可以通过A类的对象来调用A类中的触发事件的函数。这是唯一触发事件的方式。
(2)、不管是多播委托还是单播委托,在没有特殊处理的情况下,在一个线程的执行过程中去调用委托(委托对象所指向的函数),调用委托的执行是不会新起线程的,这个执行还是在原线程中的,这个对于事件也是一样的。当然,如果是在委托所指向的函数里面去启动一个新的线程那就是另外一回事了。
(3)、事件是针对某一个具体的对象的,一般在该对象的所属类A中写好事件,并且写好触发事件的方法,那么这个类A就是事件的发布者,然后在别的类B里面定义A的对象,并去初始化该对象的事件,让事件指向B类中的某一个具体的方法,B类就是A类事件的订阅者。当通过A类的对象来触发A类的事件的时候(只能A类的对象来触发A类的事件,别的类的对象不能触发A类的事件,只能订阅A类的事件,即实例化A类的事件),作为订阅者的B类会接收A类触发的事件,从而使得订阅函数被执行。一个发布者可以有多个订阅者,当发布者发送事件的时候,所有的订阅者都将接收到事件,从而执行订阅函数,但是即使是有多个订阅者也是单线程。
.NET的事件模型建立在委托机制之上,透彻的了解了委托才能明白的分析事件。可以说,事件是对委托的封装,从委托的示例中可知,在客户端可以随意对委托进行操作,一定程度上破坏了面向的对象的封装机制,因此事件实现了对委托的封装。 下面,通过将委托的示例进行改造,来完成一个事件的定义过程: public class Calculator { //定义一个CalculateEventArgs, //用于存放事件引发时向处理程序传递的状态信息
public class CalculateEventArgs: EventArgs { public readonly Int32 x, y; public CalculateEventArgs(Int32 x, Int32 y)
{ this.x = x; this.y = y;} }
//声明事件委托 public delegate void CalculateEventHandler(object sender,CalculateEventArgs e); //定义事件成员,提供外部绑定 public event CalculateEventHandler MyCalculate; //提供受保护的虚方法,可以由子类覆写来拒绝监视 protected virtual void OnCalculate(CalculateEventArgs e) { if (MyCalculate != null) { MyCalculate(this, e); } } //进行计算,调用该方法表示有新的计算发生 public void Calculate(Int32 x, Int32 y) { CalculateEventArgs e = new CalculateEventArgs(x, y); //通知所有的事件的注册者 OnCalculate(e); } }
示例中,对计算器模拟程序做了简要的修改,从二者的对比中可以体会事件的完整定义过程,主要包括: l 定义一个内部事件参数类型,用于存放事件引发时向事件处理程序传递的状态信息,EventArgs是事件数据类的基类。 l 声明事件委托,主要包括两个参数:一个表示事件发送者对象,一个表示事件参数类对象。 l 定义事件成员。
l 定义负责通知事件引发的方法,它被实现为protected virtual方法,目的是可以在派生类中覆写该方法来拒绝监视事件。 l 定义一个触发事件的方法,例如Calculate被调用时,表示有新的计算发生。 一个事件的完整程序就这样定义好了。然后,还需要定义一个事件触发程序,用来监听事件: //定义事件触发者 public class CalculatorManager { //定义消息通知方法 public void Add(object sender, Calculator.CalculateEventArgs e) { Console.WriteLine(e.x + e.y); } public void Substract(object sender, Calculator.CalculateEventArgs e) { Console.WriteLine(e.x - e.y); } } 最后,实现一个事件的处理程序: public class Test_Calculator { public static void Main() { Calculator calculator = new Calculator(); //事件触发者 CalculatorManager cm = new CalculatorManager(); //事件绑定 calculator.MyCalculate += cm.Add; calculator.Calculate(100, 200); calculator.MyCalculate += cm.Substract;
calculator.Calculate(100, 200); //事件注销 calculator.MyCalculate -= cm.Add; calculator.Calculate(100, 200); } }