关于C#委托(“指派任务”)与事件(“年终分红”)的研究
对初学C#的人来说,对委托与事件的理解可能仅停留在字面的概念上,不愿意去灵活利用它们来为应用程序服务。其实,理解清楚其中的原理并熟练搬到实际项目中,可能会使我们的程序更加通俗易懂和更好的维护。我们不妨从一个小例子入手,去理解委托与事件的精髓所在。
class Greeting
{
public void SpeakChinese(string name)
{
Console.WriteLine("你好," + name);
}
//Speak将委托调用SpeakByChinese(中文版问候)
public void Speak(string name)
{
SpeakChinese(name);
}
}
//以如下方式调用:
Greeting greeting = new Greeting();
greeting.Speak("Miracle");//你好,Miracle
现在要升级该程序支持全球化,因此外国人看不懂"你好"是什么意思,因此需要增加英文版的问候方式。由于升级后Speak也要做相应修改以支持中英文的"委托"调用,因此必须增加一个枚举作为参数传递给Speak。
class Greeting
{
public void SpeakChinese(string name)
{
Console.WriteLine("你好," + name);
}
public void SpeakEnglish(string name)
{
Console.WriteLine("Welcome," + name);
}
//Speak委托调用中英文问候方式
public void Speak(string name, Language language)
{
switch (language)
{
case Language.Chinese:
SpeakChinese(name);
break;
case Language.English:
SpeakEnglish(name);
break;
}
}
}
enum Language
{
Chinese,
English
}
//以如下方式调用:
Greeting greeting = new Greeting();
greeting.Speak("Miracle", Language.Chinese);//你好,Miracle
greeting.Speak("Miracle", Language.English);//Welcome,Miracle
由此我们发现虽然支持中英文了,但是我们还希望更多的语言(如日文,法文等)的问候方式,此时又必须增加枚举变量以及增加case分支,可扩展性极差,那有没有更好的解决办法以适应新的需求呢?答案就是:委托。我们首先来看Speak的方法签名:public void Speak(string name, Language language),正因为有了枚举变量才能分辨出对应的语言问候方式,此时假设有一个参数变量(speaker)可以代表一个方法,此方法与SpeakChinese,SpeakEnglish签名一致,那么只需要将对应语言的方法传递到参数speaker中,就可以分别调用对应的问候方式。不妨将Speak的签名修改为:public void Speak(string name, *** speaker) { speaker(name); },其中***代表参数的类型,即我们所说的委托。
delegate void Speaker(string name); //与委托的方法签名完全一致
class Greeting
{
public void SpeakChinese(string name)
{
Console.WriteLine("你好," + name);
}
public void SpeakEnglish(string name)
{
Console.WriteLine("Welcome," + name);
}
public void Speak(string name, Speaker speaker)
{
speaker(name);
}
}
//以如下方式调用:
Greeting greeting = new Greeting();
greeting.Speak("Miracle", greeting.SpeakChinese);//你好,Miracle
greeting.Speak("Miracle", greeting.SpeakEnglish);//Welcome,Miracle
从委托中我们发现:输出结果与上例相同,且去掉了枚举以及switch的case分支,从而使程序具有很好的扩展性。
总结:委托其实就是一种特殊的对象类型,定义了方法的模板,使之该方法可以作为参数传递给另一方法,其特殊性在于:以前的对象都包含数据和方法,而委托仅仅包含方法的地址(类似于C++的指针)。那么理解了委托的基本原理,委托其实就是跟其他类型(如:string)一样的类型,但是不同之处在于:将多个方法绑定("指派")到同一个委托,并依次调用对应的方法,这就是我们常说的多播委托。
delegate void Speaker(string name); //与委托的方法签名完全一致
class Greeting
{
public void SpeakChinese(string name)
{
Console.WriteLine("你好," + name);
}
public void SpeakEnglish(string name)
{
Console.WriteLine("Welcome," + name);
}
public void Speak(string name, Speaker speaker)
{
speaker(name);
}
}
//以如下方式调用:
Greeting greeting = new Greeting();
Speaker speaker = greeting.SpeakChinese;
speaker += greeting.SpeakEnglish;//给委托再绑定一个方法
greeting.Speak("Miracle", speaker);//将先后调用SpeakChinese,SpeakEnglish
/* 你好,Miracle
* Welcome,Miracle */
speaker -= greeting.SpeakEnglish;//给委托解除绑定
greeting.Speak("Miracle", speaker);//你好,Miracle:将调用SpeakChinese
事件用来实现应用程序的消息传送,过程应该是这样:事件接收器(事件源)接收来自事件发送器(对系统事件而言就是.NET运行库)的消息以响应处理,但事件发送器并不知道将消息发送给哪个接收器,此时发送器定义与接收器事件对应的委托(可响应一批事件),接收器仅需要将事件处理程序(签名与委托一致)注册到事件中实现与发送器的消息通信。我们不妨想象事件的签名:事件接收器.事件名(委托)+=事件处理程序(事件),以按钮为例:Button.Click += EventHandler(Button_Click);可以在将一个事件注册到多个事件源,同样一个事件源也可以实现多个事件(类似于多播委托,但不能保证调用顺序)。我们再来看一下事件的签名:void Button_Click(object sender, EventArgs e);其中sender为事件源,可根据它来区分是哪个事件源正在执行事件处理程序。EventArgs是包装有关事件的基类,可继承该类以实现更复杂的事件响应。通常以object_eventName命名。也可以采用匿名方法来进行事件调用。
Button.Click += EventHandler(Button_Click);
Button.Click += (sender, e) => {//事件响应代码}
下面我们用一个小例子来说明事件的创建、引发、接收和取消的全过程:
首先描述一下需求: 我们希望在按钮事件中实现检查当前时间的秒数是否超过30秒,如果超过则取消事件执行同时显示错误信息("不正确的时间!"),否则输出当前的时间。
1. 新建一个窗体frmEvent,并添加一个按钮btnRaise和一个文本lblInfo. 添加委托和事件声明:
public partial class frmEvent : Form
{
//定义事件委托
public delegate void ActionEventHandler(object sender, ActionCancelEventArgs e);
//定义事件(可为非静态)
public static event ActionEventHandler Action;
//也可以使用如下方式定义事件
/*
private static ActionEventHandler action;
public static event ActionEventHandler Action
{
add
{
action += value;
}
remove
{
action -= value;
}
}
*/
}
2. 由于需要,我们扩展已有的EventArgs类以适应我们的需求:
public class ActionCancelEventArgs : CancelEventArgs
{
public ActionCancelEventArgs() : this(false) { }
public ActionCancelEventArgs(bool cancel) : this(cancel, String.Empty) { }
public ActionCancelEventArgs(bool cancel, string message)
: base(cancel)
{
this.Message = message;
}
public string Message { get; set; }
}
3. 现在委托与事件都已定义,接下来需要定义事件方法(签名必须与委托一致):
protected void OnAction(object sender, ActionCancelEventArgs e)
{
if (Action != null)
{
Action(sender, e);
}
}
4. 我们还需要定义事件处理程序(可嵌套在frmEvent的InitializeComponent()方法中),这里我们单独写一个处理业务:
public class EventBusiness
{
string time = String.Empty;
public EventBusiness()
{
/*
frmEvent.Action += (sender, e) =>
{
e.Cancel = !DoAction();
if (e.Cancel)
{
e.Message = "Error";
}
};
*/
//可按提示按两次tab就可以绑定事件和事件处理程序
frmEvent.Action += new frmEvent.ActionEventHandler(frmEvent_Action);
}
void frmEvent_Action(object sender, ActionCancelEventArgs e)
{
e.Cancel = !DoAction();
if (e.Cancel)
{
e.Message = "不正确的时间!";
}
}
//具体业务
public bool DoAction()
{
var now = DateTime.Now;
if (now.Second < 30)
{
time = "当前时间:" + now.ToLongTimeString();
return true;
}
return false;
}
public string Time
{
get { return time; }
}
}
5. 最后,我们在窗体的按钮事件中触发事件:
public partial class frmEvent : Form
{
//...
//触发事件
private void btnRaise_Click(object sender, EventArgs e)
{
var eb = new EventBusiness();
var ev = new ActionCancelEventArgs();
OnAction(sender, ev);
lblInfo.Text = ev.Cancel ? ev.Message : eb.Time;
}
}
以上我们基本讲述了委托与事件的关系以及如何利用事件进行消息传递的整个过程,希望以后在项目中应用有更深的理解。