委托和事件在.NET程序开发中的重要性可说是再怎么强调也不嫌过份。无论是面向对象,WinForm还是Web开发中,都经常可见其身影。但是,很多初学者对于这两个概念总会感到很茫然。乍一听这两个名词就让人觉得太过干涩,好比一味苦药难于入口。但当你理解它们之后,就又会它们觉得其实是这么简单,只是不知为何被取了这么两个怪名。
事实上,若你真正理解之后,会明白其实这两个名字取得非常形象生动,正反映出二者的作用。就让我们一一来理解他们吧。
委托(Delegate)
雷锋,是中国家喻户晓的一位英雄人物,而雷锋精神,更可以说是伴随我们一起成长的心灵导师。一句话说就是:从不利己,时时刻刻都只为他人着想。而委托正是C#语言中的一位活雷锋,它只有在调用其他方法时才有存在价值(匿名委托除外)。
在C#的面向对象中,委托与类、接口属于同一级别。它使用关键字Delegate定义,属于引用类型。你可以把它理解成一个特殊的方法,因为它拥有返回值、参数和委托名。但与方法不同的是,他没有方法体,里面不能放置其他代码。也就是说,它必须去调用其它方法才有存在价值。就像我们前面所说的,它就是一个专用来调用其它方法的方法。它是方法的发动机,不仅可以调用某个指定的方法,更可以一次性调用多个方法。那你可能要问了,我干嘛要用一个方法去调用另外一个方法,不是多此一举吗?不,这并不是你想像的那么简单。在我们开发中经常需要使用到委托这种应用。比如说,当你需要将一个方法作为另一个方法的参数时,使用委托来定义就比较合理了。在这种应用环境下,你可以把委托看作一个类型,这个类型是专门接收指定的各种方法,只要这些方法的返回值、参数类型和个数与委托的类型个数相同即可。
实际上,委托并不是C#语言中独创的,在C++中就有函数指针的概念,但委托不同于函数指针。它最大的优点就是“类型安全”,我们在前面讲.NET的类型系统时提到过,由下面我们来看一个简单的委托定义。
using System;
using System.Collections.Generic;
using System.Text;
namespace ConsoleApplication1
{
class MyDelegate
{
/// <summary>
/// 游戏角色攻击的委托
/// </summary>
/// <param name="role">对方角色</param>
/// <returns>所获经验值</returns>
public delegate int AttackLevel(Role role);
}
}
在上面的例子中,定义了一个简单的委托,用于完成对任何一个具有Role型参数和一个int型返回值的方法的调用。从类似与方法的XML注释可以看出,委托与方法的定义十分类似。
定义好了委托,就可以对它进行使用了,但记住,委托一定要调用别的方法才有效果。所以下面还需要定义一个或多个具有相同特性的方法。请看以下示例:
![](https://www.cnblogs.com/Images/OutliningIndicators/ContractedBlock.gif)
Code
using System;
using System.Collections.Generic;
using System.Text;
namespace ConsoleApplication1
{
class Program
{
static void Main(string[] args)
{
Role role1 = new Role();
role1.Name = "张飞";
Role role2 = new Role();
role2.Name = "夏侯惇";
//为委托添加“普通攻击”的方法
AttackLevel attack = new AttackLevel(role1.Normal);
//执行委托,对role2进行攻击,获取经验值
int Exper=attack(role2);
//为role1添加对应的经验值
role1.Experience += Exper;
Console.WriteLine("{0}向{1}发动攻击,所获经验值为{2}点!", role1.Name, role2.Name, Exper.ToString());
Console.WriteLine("{0}的生命值降为{1}点!", role2.Name, role2.LifeValue.ToString());
//将上次“普通攻击”的方法从委托中移除,加入“中等攻击”的方法
attack -= role1.Normal;
attack += role1.Medium;
Exper = attack(role2);
role1.Experience += Exper;
Console.WriteLine("{0}向{1}发动攻击,所获经验值为{2}点!", role1.Name, role2.Name, Exper.ToString());
Console.WriteLine("{0}的生命值降为{1}点!", role2.Name, role2.LifeValue.ToString());
//将上次“中等攻击”的方法从委托中移除,加入“高级攻击”的方法
attack -= role1.Medium;
attack += role1.Advanced;
Exper = attack(role2);
role1.Experience += Exper;
Console.WriteLine("{0}向{1}发动攻击,所获经验值为{2}点!", role1.Name, role2.Name, Exper.ToString());
Console.WriteLine("{0}的生命值降为{1}点!", role2.Name, role2.LifeValue.ToString());
Console.WriteLine("{0}现在的经验值{1}点!", role1.Name, role1.Experience.ToString());
}
/// <summary>
/// 角色进行攻击的委托
/// </summary>
/// <param name="role">对方角色</param>
/// <returns>所获经验值</returns>
public delegate int AttackLevel(Role role);
}
public class Role
{
/// <summary>
/// 初始化角色基本信息
/// </summary>
public Role()
{
this.LifeValue = 100;
this.Experience = 0;
}
#region 角色基本信息
private string _Name;
/// <summary>
/// 角色姓名
/// </summary>
public string Name
{
get { return _Name; }
set { _Name = value; }
}
private int _LifeValue;
/// <summary>
/// 生命值
/// </summary>
public int LifeValue
{
get { return _LifeValue; }
set { _LifeValue = value; }
}
private int _Experience;
/// <summary>
/// 经验值
/// </summary>
public int Experience
{
get { return _Experience; }
set { _Experience = value; }
}
#endregion 角色基本信息
#region 角色战斗方法
/// <summary>
/// 普通攻击
/// </summary>
/// <param name="role">对方角色</param>
/// <returns>所获经验值</returns>
public int Normal(Role role)
{
role.LifeValue -= 10;
return 100;
}
/// <summary>
/// 中等攻击
/// </summary>
/// <param name="role">对方角色</param>
/// <returns>所获经验值</returns>
public int Medium(Role role)
{
role.LifeValue -= 20;
return 200;
}
/// <summary>
/// 高级攻击
/// </summary>
/// <param name="role">对方角色</param>
/// <returns>所获经验值</returns>
public int Advanced(Role role)
{
role.LifeValue -= 30;
return 300;
}
#endregion 角色战斗方法
}
}
这个示例模拟一个游戏中角色发动攻击的效果。在Role类中,拥有三个进行攻击的方法分别是:Normal(Role role),Medium(Role role)和Advanced(Role role),分别代表角色的普通攻击、中等级攻击和高级攻击方法。这个三个方法拥有相同类型的参数,相同类型的返回值,不同的方法名,因此在使用时,可用同一个委托来进行调用,但要保证这个委托也只能是int型返回值和Role型的参数。
在该示例中,第一次将普通攻击方法对应到委托时,使用委托的构造函数来完成:
AttackLevel attack = new AttackLevel(role1.Normal);
这里创建了一个AttackLevel的委托,并将一个匹配的攻击方法添加到委托中,你可以将委托想象成一个集合,但不是采用集合常用的Add方法,而是在创建委托时,用构造函数来添加。 委托被创建后,可使用累计的做法加入其它对应方法,比如:
attack += role1.Medium;
这句的作用就是将“中等攻击”的方法添加到委托中,既然可以添加方法,委托也允许减少方法,例如:
attack -= role1.Normal;
你甚至可以一次性将所有可匹配委托的方法同时加入到委托中,并赋予一个相同的参数,让三个方法同时执行,试想一下,若主函数做如下修改将会是什么结果:
static void Main(string[] args)
{
Role role1 = new Role();
role1.Name = "张飞";
Role role2 = new Role();
role2.Name = "夏侯惇";
//为委托添加“普通攻击”的方法
AttackLevel attack = new AttackLevel(role1.Normal);
attack += role1.Medium;
attack += role1.Advanced;
attack(role2);
}
匿名方法
从以上学习中,我们可以看出:委托只是一个方法的载体,它需要有一个或多个现成的方法,通过将方法添加到委托中来使用。但实际使用中,我们并不一定需要定义类和方法,可以直接在委托中执行一段程序。此时因为并不存在一个方法实体,因此它被称为匿名方法。看如下示例:
![](https://www.cnblogs.com/Images/OutliningIndicators/ContractedBlock.gif)
Code
using System;
using System.Collections.Generic;
using System.Text;
namespace ConsoleApplication1
{
class Program
{
static void Main(string[] args)
{
Role role1 = new Role();
role1.Name = "张飞";
//定义匿名委托,完成为角色加血
AddLife addLife = delegate(Role role)
{
role.LifeValue += 50;
return true;
};
//判断加血是否成功
if (addLife(role1))
{
Console.WriteLine("{0}加血成功,现在血量为点{1}!",role1.Name,role1.LifeValue);
}
}
/// <summary>
/// 角色加血的委托
/// </summary>
/// <param name="role">角色</param>
/// <returns>是否完成</returns>
public delegate bool AddLife(Role role);
}
public class Role
{
/// <summary>
/// 初始化角色基本信息
/// </summary>
public Role()
{
this.LifeValue = 100;
this.Experience = 0;
}
#region 角色基本信息
private string _Name;
/// <summary>
/// 角色姓名
/// </summary>
public string Name
{
get { return _Name; }
set { _Name = value; }
}
private int _LifeValue;
/// <summary>
/// 生命值
/// </summary>
public int LifeValue
{
get { return _LifeValue; }
set { _LifeValue = value; }
}
private int _Experience;
/// <summary>
/// 经验值
/// </summary>
public int Experience
{
get { return _Experience; }
set { _Experience = value; }
}
#endregion 角色基本信息
}
}
其中,这段代码就是匿名方法:
AddLife addLife = delegate(Role role)
{
role.LifeValue += 50;
return true;
};
可以看出,这和普通方法的定义有很大不同,首先它的返回值和参数类型都是由委托AddLife来定义,另外通过delegate关键字为其提供参数。在调用方法时,也是通过委托来调用。
匿名方法的优点是减少了要编写的代码。方法仅在由委托使用时才定义。这有助于降低代码的复杂性。
使用匿名方法时,必须遵循一下规则:
1. 匿名方法中的跳转语句不能跳到该匿名方法的外部,匿名方法外部的跳转语句不能跳到该匿名方法的内部。
2. 在匿名方法内部不能访问不安全的代码,也不能访问匿名方法外部使用的ref和out参数。
3. 如果需要用匿名方法多次编写同一个功能,就不要使用匿名方法,以提高代码执行速度。
事件(Event)
事件与委托休戚与共,有了委托才有事件,委托是事件的前提条件,事件是委托的扩展。但很多初学者往往在事件上感到迷茫,这是因为市面上一些书籍谈到事件时动辄就时消息机制,事件发布者,事件接受者这些干涩的解释。这里不免要批评几句,现在国内很多出版社再拿到一本国外的技术书籍时为了能更快让书上市,在翻译时不顾科学原则,只一味要求译者的速度,对质量却毫不在意。再加上译者的水平也是参差不齐,不是只会翻译不懂技术,就是只懂技术不会翻译。而出版社确管不了这么多,只要你给我翻译出来了,我就敢出版。殊不知,在技术领域,一个小小文字上的差异就可能导致整个词句与原意大相径庭。比方说:属性和特性。这两个词对应的英文单词分别是:property和attribute。而现在有很多外表装饰得非常漂亮的书籍中就把attribute叫做属性。实际上这个东西完全是不一样的概念。具体请见anytao的这篇文章:http://www.cnblogs.com/anytao/archive/2007/04/19/must_net_03.html
废话不多说,还是回到我们刚才的话题。其实事件很容易理解,它的作用用一句话来概括就是让程序在某个特定时刻执行一段指定的代码。注意这里说的特定时刻指的不是某一分某一秒,而是当一个规定的状态到达时,触发执行指定代码。比如在Windows窗口打开或关闭时,都会被系统认为是一个状态到达。打开窗口的状态到达时,就可以执行代码为窗口中的文本框赋一个初始值。再比如当某个网页向服务器端提交数据结束时,也可以被看做是一个状态到达。这时,可执行代码为用户返回一个确认消息。
如果你还不能理解这个过程,我们可以将它再说通俗一点。事件就好比A和B两个人走路,A不小心踩到了B的脚,B马上就感到疼,并且提醒B走路要小心。这时,A就是事件的触发者,B就是事件的接收者。因为A踩到B的脚,触发了B的“疼”这个事件。B在疼过后就会开始指责A,告诫她走路要小心。在这个过程中,B就拥有一个被踩事件(B有受虐狂倾向)。当A踩到B时,意味着踩的状态达到,B的被踩事件被触发。另外,这个踩的状态还可以被细分,分为正在被踩事件和被踩后事件。正在被踩事件执行代码让B感觉到疼;被踩后事件执行的代码让B开始责怪A。明白了这个概念,我们再回到.NET中来,我们的WINFORM窗体或WEB窗体也具有这样的特性,比如窗体的关闭可分为FormClosing和FormClosed两个事件,前者表示窗口已关闭并指定关闭原因前发生,后者表示窗口已关闭并指定关闭原因后发生。
只要了解这一点,你就大致了解了事件的机制和使用事件的原因。最后我们来看看代码的实现。以下,还是举一个较好理解的例子。先看下面这段程序。
![](https://www.cnblogs.com/Images/OutliningIndicators/ContractedBlock.gif)
Code
using System;
using System.Collections.Generic;
using System.Text;
namespace ConsoleApplication1
{
class Program
{
static void Main(string[] args)
{
Person p = new Person();
p.ageChanged += new Person.ageEvent(myAge_Changed);
p.ageChanged += new Person.ageEvent(School.goSchool);
p.ageChanged += new Person.ageEvent(Marriage.isMarried);
School sc = new School();
int input = 1;
while (input > 0)
{
try
{
Console.Write("请输入数字:");
input = Convert.ToInt32(Console.ReadLine());
p.Age = input++;
}
catch
{
input = -1;
}
Console.WriteLine("");
}
//Console.Read();
}
// 对Change事件处理的程序
private static void myAge_Changed(object sender, EventArgs e)
{
Console.WriteLine("我的年龄是:{0} 岁", ((Person)sender).Age);
}
}
/// <summary>
/// 一个人的类,很老套;
/// </summary>
public class Person
{
//定义一个委托
public delegate void ageEvent(object sender, EventArgs arg);
//用上面的委托,定义一个事件
public event ageEvent ageChanged;
// 用以触发Change事件
protected virtual void OnAgeChanged(EventArgs e)
{
if (this.ageChanged != null)
this.ageChanged(this, e);
}
//
private int _age = 0;
public int Age
{
get { return _age; }
set
{
this._age = value;
// 文本改变时触发Change事件
this.OnAgeChanged(new EventArgs());
}
}
}
public class School
{
//它的签名要与person类中的事件委托的签名一样;
public static void goSchool(object sender, EventArgs arg)
{
int age = ((Person)sender).Age;
if (age >= 6 && age < 10)
{
Console.WriteLine("你的年龄是:{0} 岁,你应该上学了!", age);
}
}
}
public class Marriage
{
//它的签名要与person类中的事件委托的签名一样;
public static void isMarried(object sender, EventArgs arg)
{
int age = ((Person)sender).Age;
if (age >= 22 && age < 40)
{
Console.WriteLine("你的年龄是:{0} 岁,你结婚了吗?", age);
}
}
}
}