C# 篇基础知识5——委托和事件

时间:2024-09-29 13:03:32

事件处理程序是基于“委托”机制运行的。

1.委托

1)委托的定义和使用

有时需要将一个函数作为另一个函数的参数,这时就要用到委托(Delegate)机制。例如设计一个马戏表演函数:

//定义委托

delegate void AnimalPlay(string name);

static void CircusStart(AnimalPlay animalPlay, string name){ animalPlay(name); }

这里AnimalPlay是委托的类型,而animalPlay是委托,调用时可以:

//把函数DogPlay()转换为AnimalPlay 型委托

AnimalPlay deleDogPlay = new AnimalPlay(DogPlay);

//把委托deleDogPlay 传给函数CircusStart()

CircusStart(deleDogPlay, "Good evening");

函数CircusStart()的第一个参数是动物表演函数,传给它什么样的函数,它就进行什么动物的表演,C#中函数的参数都有严格的类型,怎么表示这种“函数参数”的类型呢?这就需要用到委托(Delegate)。委托相当于定义了一种“函数类型”,规定了这类函数的参数类型和返回值类型。委托用关键字delegate 声明:

C# 篇基础知识5——委托和事件

上面的语句定义了一种名为AnimalPlay的委托,与它匹配的函数必须具有相同的签名。当需要把某个函数作为参数时,可以先把它转换为委托实例,然后把得到委托实例为参数传递给调用它的函数,可以看出委托实例deleDogPlay 实际上相当于函数DogPlay()的别名:

C# 篇基础知识5——委托和事件

在新版本的.NET 中,可以直接传递函数名称,省去把函数转换为委托实例的过程。.Net 编译器会暗中把DogPlay()函数转换为AnimalPlay 委托的实例,从而简化编程过程。例如:

delegate void AnimalPlay(string name);

static void CircusStart(AnimalPlay animalPlay, string name){ animalPlay(name); }

static void DogPlay(string greetings)

{…}

static void Main(string[] args)

{

//直接传递DogPlay()函数

CircusStart(DogPlay, "Snoopy");

}

综上所述,利用委托可以实现以函数为参数,提高程序的通用性。实际上委托也是由类实现的,当我们创一个从System.Delegate 派生出来的类,类中有一个调用列表,列表中包含着指向被委托函数的引用。与C++指针相比,委托是一种类型安全的方式,并能实现很多其它功能。

2)多播委托

可以向一个委托实例中注册多个函数,这种包含多个函数的委托实例称为多播委托(Multicast Delegate),那些被委托的函数的引用都存储在多播委托的调用列表中,当调用多播委托时,会按一定顺序依次调用列表中的所有函数。例如:

static void Main(string[] args) {

//多播委托 AnimalPlay animalsPlay = new AnimalPlay(DogPlay);

animalsPlay += new AnimalPlay(CatPlay);

animalsPlay += new AnimalPlay(LionPlay);

CircusStart(animalsPlay,"Good morning"); }

向多播委托实例中注册函数animalsPlay += new AnimalPlay(CatPlay);,注销函数animalsPlay -= new AnimalPlay(CatPlay);。

多播委托的返回值一般为void,如果为非void 类型,多播委托可能有多个返回值(因为会调用多个函数),这时多播委托将返回最后一个方法的返回值。但实际中不推荐这样应用,因为这些方法的调用顺序未正式定义,因此应避免编写依赖于特定调用顺序的代码。

3)匿名函数

创建委托实例时不仅可以使用已有的函数,而且可以直接使用匿名函数(Anonymous Function)。看下面这个例子:

public delegate void EatFoodDelegate(Person p);

public class Person

{  public string Name { get; set; }

public int Age { get; set; }

public Person(string name, int age)

{   Name = name;  Age = age;   }

public EatFoodDelegate eatFoodDelegate;

public void eating()

{  if (eatFoodDelegate != null)

{  eatFoodDelegate(this); }

}

}

static void Main(string[] args)

{   Person chinesePerson = new Person("小明",25);

//方式一:将用实际函数创建的委托实例注册到委托,早期C#常用方法

chinesePerson.eatFoodDelegate += new EatFoodDelegate(chineseEat);

chinesePerson.eating();

Person englishPerson = new Person("Ivan",25);

//方式二:直接将匿名函数赋值给一个委托实例

englishPerson.eatFoodDelegate = delegate (Person p)

{Console.WriteLine("I'm {0},I am {1} , I eat MianBao", p.Name, p.Age);};

englishPerson.eating(); }

}

static void chineseEat(Person p)

{Console.WriteLine("{0}, {1}岁,我吃馒头",p.Name,p.Age);}

(4) Lambda表达式

C#3.0 引入了匿名函数——Lambda 表达式,有了Lambda 表达式,可以用非常简洁的方式定义匿名函数。例如:

englishPerson.eatFoodDelegate = delegate (Person p)

{Console.WriteLine("I'm {0},I am {1} , I eat MianBao", p.Name, p.Age);};

可以写成:

englishPerson.eatFoodDelegate =(Person p)=>

{Console.WriteLine("I'm {0},I am {1} , I eat MianBao", p.Name, p.Age);}

Lambda表达式是一种简洁的定义匿名函数的方法,用Lambda表达式构造的匿名函数看上去就像一个计算表达式,它使用“=>”符号来连接参数和事件处理代码,极大的增强了匿名函数的可读性。例如将匿名函数赋值给一个Integrand委托的实例f3:

C# 篇基础知识5——委托和事件

不但如此,我们还可以省略Lambda 表达式参数列表中的参数类型和“return”关键字, 和普通方法一样,Lambda表达式可以没有参数,也可以有多个参数,当无参数或有多个参数时,“=>”左边必须有(),当仅有一个参数时,可以省略:

Integrand f3=()=>{3*2+5};

Integrand f3=x=>{3*x+5};

Integrand f3=(x,y)=>{3*x+5*y};

5)宽松委托

从C#3.0 开始,出现了更为宽松的委托,被委托函数的参数类型可以比委托要求的更大、更宽泛,被委托函数的返回值类型可以比委托要求的更小、更精确。

Public class Animal{}

Public class Cat:Animal{}

Public delegate Animal AnimalHandler(Animal a);

Public static Cat CatPlay(Object o)

{return null;}

则下面的语句是合法的:

AnimalHandler handler=new AnimalHandler(CatPlay);

被委托函数的参数类型可以是委托参数的基类(逆变),被委托函数的返回值类型可以是委托返回值的派生类(协变)。

2.事件处理机制

(1).NET事件概述

Windows 应用程序是事件驱动的,当一个窗体应用程序启动后,系统就不停的检测是否有事件发生,如果检测到事件,就执行对应的事件处理程序。单击鼠标、敲击键盘都会触发事件(Raise Event),系统都会找到并执行对应的事件处理程序。把触发事件的对象称为事件的发送者(Event Sender),响应事件的对象称为事件的接收者(Event Receiver)。例如一个窗体中按钮触发事件的过程,按钮就是事件的发送者,事件的接收者是窗体(因为事件处理程序是窗体的函数成员)。

Public partial class Form1:Form

{ private void btnOK_Click(object sender,EventArgs e)

{ MessageBox.Show(“Hello”);}  }

当事件发生时,系统是如何找到对应的事件处理程序的呢,当然不可能根据事件处理程序的名称,因为.NET程序员要以随意定义事件处理程序的名称。实际上,只能是根据事件的名称来寻找相应的事件处理程序。.NET 中预定义了许多种专门用于事件的委托类型,比如EventHandler、KeyEventHandler、MouseEventHandler 等

public delegate void EventHandler(object sender, EventArgs e);

EventHandler是一个很常用的事件委托,这个委托接收两个参数,一个是发送事件的发送者,一个是事件本身的参数。Control类,就会发现该类定义了大量事件成员,如图所示:

C# 篇基础知识5——委托和事件

其中有我们很熟悉的Click、DoubleClick、KeyPress等。

public event EventHandler Click;

public event EventHandler DoubleClick;

public event EventHandler KeyPress;

……

可出看出,事件实际上就是一个委托实例而已,事件和委托的关系,就是对象和类的关系,事件和方法、变量、属性等一样,都是控件类的成员,只是声明事件时必须用event关键字。默认情况下,系统已经将窗体程序的各种行为,与定义的系统事件一一对应起来了,只要有一种预定义的行为发生,则会触发相应的事件,然后就会调用事件的处理程序。将自定义的处理程序添加到事件,非常简单,就是将一个函数注册到委托实例的过程,例如:

btnOK.Click += new System.EventHandler(this.btnOK_Click);

此后,用户函数的引用就被保存在事件btnOK.Click的调用列表中,系统通过事件的调用列表,就能轻松地找到对应的事件处理程序。注意:event关键字的作用是把事件封装起来,即只能在声明事件的类的内部触发事件,在类外部只能注册事件而不能触发事件。比如对于在btnOK里声明的Click事件,语句“btnOK.Click()”只能出现在btnOK类内部,不能出现在类外部。可以在一个事件中注册多个事件处理程序。当事件发生时,系统就会依次调用事件中所有的处理程序。

总之,.NET就是通过委托机制把系统和应用程序中的事件处理程序联系起来的。

(2)自定义事件

要创建一个事件驱动的程序需要下面的步骤:

1.声明关于事件的委托;

2.声明事件;

3.编写触发事件的函数;

4.创建事件处理程序;

5.注册事件处理程序;

6.在适当的条件下触发事件

按照这6个步骤,.NET程序员可以自定义事件及其处理程序。例如

//事件发送者

Class Dog{

//1.声明关于事件的委托

public delegate void AlarmEventHandler(object sender,EventArgs e);

//2.声明事件

public event AlarmEventHandler Alarm;

//3.编写引发事件的函数

public void OnAlarm()

{  //先判断有无事件程序,然后再调用

if(this.Alarm!=null) this.Alarm(this,new EventArgs());}

}

//事件接收者

Class Host

{

//4.编写事件处理程序

void HostHandleAlarm(object sender,EventArgs e)

{  Console.WriteLine(“主要:抓住了小偷”);}

//5.注册事件处理程序

public Host(Dog dog)

{  dog.Alarm+=new Dog.AlarmEventHandler(HostHandleAlarm);}

}

//6.现在来触发事件

class Program

{

static void Main(string [] args)

{Dog dog=new Dog();

Host host=new Host(dog);

DateTime now = new DateTime(2011,12,31,23,59,55);

DateTime midnight = new DateTime(2012, 1, 1, 0, 0, 0);

Console.WriteLine("时间一秒一秒地流逝,等待午夜的到来... ");

while(true)

{

Console.WriteLine(“当前时间:”+Now);

//午夜零点小偷到达,看门狗引发Alarm 事件

if(now == midnight)

{  Console.WriteLine("\n 月黑风高, 小偷悄悄地摸进了主人的屋内...");

//引发事件

Console.WriteLine("\n 狗报警: 有小偷进来了,汪汪~~~~~~~");

dog.OnAlarm();

break;   }

System.Threading.Thread.Sleep(1000); //程序暂停一秒

now = now.AddSeconds(1); //时间增加一秒

}  //while

} //Main

} //program

当午夜时分小偷到达时,dog 调用dog.OnAlarm()函数,从而触发Alarm 事件,于是系统找到并执行了注册在Alarm 事件中的事件处理程序HostHandleAlarm()。引发事件的代码(即OnAlarm()函数)和事件处理程序是分离的,引发事件的代码只管调用“Alarm()”,而事件处理程序另外独立定义,它们之间通过事件Alarm 联系起来。事件处理程序接受两个参数,一个是事件的发送者sender,一个是事件参数e,事件参数用于在发送者和接收者之间传递信息。.NET 提供了100 多个事件参数类,它们都继承于EventArgs 类。

一般情况下,使用.NET自带的类足够了,但为了讲清原理,来自定义一个事件参数类。

public class AlarmEventArgs : EventArgs

{  public int numberOfThief;

public AlarmEventArgs(int numberValue)

{ numberOfThief= numberValue;}

}

class Dog

{

//1.声明关于事件的委托;

public delegate void AlarmEventHandler(object sender, AlarmEventArgs e);

//2.声明事件;

public event AlarmEventHandler Alarm;

//3.编写引发事件的函数,注意多了个参数;

public void OnAlarm(AlarmEventArgs e)

{  if(this.Alarm != null)

this.Alarm(this, e);  }

}

//事件接收者

class Host

{

//4.编写事件处理程序,参数中包含着numberOfThief 信息

//如果只有一个小偷,主人抓住小偷;如果小偷多于一个,主人报警

void HostHandleAlarm (object sender, AlarmEventArgs e)

if (e.numberOfThief <= 1)

{  Console.WriteLine("主人: 抓住了小偷!");}

else

{Console.WriteLine("主人:打110报警,我家来了{0}个小偷!", e.numberOfThief);}

//5.注册事件处理程序

public Host(Dog dog)

{ dog.Alarm += new Dog.AlarmEventHandler(HostHandleAlarm);}

}

//6.现在来触发事件

class Program

{

static void Main(string[] args)

{ Dog dog = new Dog();

Host host = new Host(dog);

DateTime now = new DateTime(2011,12,31,23,59,55);

DateTime midnight = new DateTime(2012, 1, 1, 0, 0, 0);

Console.WriteLine("时间一秒一秒地流逝,等待午夜的到来... ");

while (true)

{  Console.WriteLine("当前时间: " + now);

//午夜零点小偷到达,看门狗引发Alarm 事件

if(now == midnight)

{ Console.WriteLine("\n 月黑风高的午夜,小偷悄悄地摸进了主人的屋内...");

//引发事件

Console.WriteLine("\n 狗报警: 有小偷进来了,汪汪~~~~~~~");

AlarmEventArgs e = new AlarmEventArgs(3); //创建事件参数

dog.OnAlarm(e);

break;  }

System.Threading.Thread.Sleep(1000); //程序暂停一秒

now = now.AddSeconds(1); //时间增加一秒

}

}

}