Delegate,委托(或代理)是这样一种数据类型:它的变量可以引用到某一个符合要求的方法上,通过委托可以间接地调用该方法。
C#的委托类似于C语言的函数指针,区别在于C#的委托是面向对象的。
我们可以这样认为:在纯粹的面向对象语言C#中,方法也是一种特殊对象,对象的行为就是方法的行为,对象的属性是方法的返回值和参数列表。既然可以将方法认定为对象,那该对象也可以抽象出类来。这个类,就是Delegate类,即委托类。
例如,我们要为形如 void DoSomething(int a, double b) 的方法创建委托,则语法如下:
- delegate void DelegateHandler(int a, double b);
其中,DelegateHandler就是一个代理类型,可以认为它是一个“类”,是所有 返回类型为void,具备两个参数int a, double b的方法 的抽象,使用这个类型可以创建变量:
既然是变量,就可以给变量赋值,但这是委托,是一个方法的类,所以变量必须引用到一个方法上,例如如下方法:
- public class Test {
- public void TestDelegate(int a, double b) {
- Console.WriteLine("a = {0}, b = {1}", a, b);
- }
- }
有了这样一个方法,就可以用它对委托变量赋值了。
- Test t = new Test();
- DelegateHandler handler = t.TestDelegate;
- handler(15, 0.5);
通过将一个函数的名称作为变量值赋值给委托变量,从而使用委托变量(handler)间接地可以调用Test对象t的TestDelegate方法。即,我们将对象t的TestDelegate委托给了DelegateHandler类型的变量handler来执行。
一个完整的代码例子:
- using System;
- using System.Collections.Generic;
- using System.Linq;
- using System.Text;
- namespace Edu.Study.Graphics.Delegate {
- /// <summary>
- /// 定义一个代理类型,可代理所有返回值为void,参数列表为空的方法
- /// </summary>
- public delegate void EventHandler(bool status);
- /// <summary>
- /// 开关类
- /// </summary>
- public class Switcher {
- /// <summary>
- /// 代理类型变量,该变量可以保存一个返回值类型为void,无参数列表的“方法”的引用
- /// </summary>
- private EventHandler click = null;
- /// <summary>
- /// 保存开关状态
- /// </summary>
- private bool status = false;
- /// <summary>
- /// 代理类型属性
- /// </summary>
- public EventHandler Click {
- set {
- this.click = value;
- }
- get {
- return this.click;
- }
- }
- /// <summary>
- /// 当点击开关时执行的方法
- /// </summary>
- public void OnClick() {
- // 切换开关状态
- this.status = !this.status;
- // 显示开关状态
- if (this.status == true) {
- Console.WriteLine("开关被打开。");
- } else {
- Console.WriteLine("开关被关闭。");
- }
- // 呼叫代理
- if (this.Click != null) {
- this.Click(this.status);
- }
- }
- }
- /// <summary>
- /// 电灯类
- /// </summary>
- public class Light {
- /// <summary>
- /// 灯的名称字段
- /// </summary>
- private string lightName;
- /// <summary>
- /// 构造器
- /// </summary>
- /// <param name="lightName">灯的名字</param>
- public Light(string lightName) {
- this.lightName = lightName;
- }
- /// <summary>
- /// 灯名字属性(只读)
- /// </summary>
- public string LightName {
- get {
- return this.lightName;
- }
- }
- /// <summary>
- /// 打开灯的方法
- /// </summary>
- /// <param name="status">开关状态</param>
- public void Open(bool status) {
- // 根据传入的状态值确定灯是亮的还是灭的
- if (status == true) {
- Console.WriteLine("{0}亮了!", this.LightName);
- } else {
- Console.WriteLine("{0}熄了!", this.LightName);
- }
- }
- }
- /// <summary>
- /// 风扇类
- /// </summary>
- public class Fan {
- /// <summary>
- /// 风扇类型字段
- /// </summary>
- private string fanType;
- /// <summary>
- /// 构造器,构造一个有类型的风扇
- /// </summary>
- /// <param name="fanType">风扇类型</param>
- public Fan(string fanType) {
- this.fanType = fanType;
- }
- /// <summary>
- /// 风扇类型属性,只读
- /// </summary>
- public string FanType {
- get {
- return this.fanType;
- }
- }
- /// <summary>
- /// 风扇工作方法。
- /// </summary>
- /// <param name="status">开关状态</param>
- public void Work(bool status) {
- // 根据传入的状态值确定风扇是转动还是停止
- if (status == true) {
- Console.WriteLine("{0}转动了!", this.FanType);
- } else {
- Console.WriteLine("{0}停止了!", this.FanType);
- }
- }
- }
- /// <summary>
- /// 执行测试方法
- /// </summary>
- class Program {
- static void Main(string[] args) {
- // 定义一个灯对象和一个风扇对象
- Light light = new Light("床头灯");
- Fan fan = new Fan("落地扇");
- // 定义一个开关对象
- Switcher s = new Switcher();
- // 将灯的Open方法交给开关的Click代理属性代理
- s.Click = light.Open;
- // 执行两次开关OnClick方法(即将开关点击两次)
- s.OnClick();
- s.OnClick();
- // 将风扇的Work方法交给开关的Click代理属性代理
- s.Click = fan.Work;
- s.OnClick();
- s.OnClick();
- }
- }
- }
通过上述的代码,我们可以看到,所谓的委托,就是一个可以保存方法的变量,这个非常类似于C语言中指向函数的指针,但C#的委托功能更为强大。
委托的最终目的是把一个类的某个方法传递到另一个类中去调用而无须传递前一个类的对象。即,一个类的对象可以运行另一个类的对象中的方法,但前者无须持有后者的引用。
在某些情况下,为了运行某个类对象的某个方法,但无论以类变量、超类变量或是接口变量传递这个对象都不合适,此时使用委托最为合理。
上面代码中表示的很明确,开关要能够控制灯和风扇,就必须能够访问灯对象的Open方法或风扇的Work方法,同一个开关对象可以控制灯或风扇对象,但灯和风扇既没有继承自相同的超类,也没有实现相同的接口,所以开关对象不可能去引用灯对象和风扇对象,但利用委托,则可以完美的解决该问题。
委托赋予了C#语言事件这一概念。
什么是事件?简而言之,就是一个对象的某个方法,必须在另一个对象的某个方法内部来调用。
例如电灯对象具有“亮”这个方法,但这个方法不会由灯对象自身来调用,而必须在开关对象的“打开开关”方法内来调用。也就是说,开关“打开开关”触发了灯“亮”这个事件。所以实现事件的方法很多,最简单的方法,可以让开关对象保存一个灯对象的一个引用,并在开关的“打开开关”方法内,通过这个引用调用灯的“亮”这个方法,从而实现事件触发,这种方法的缺点很明显:将特定的开关类型和特定的灯类型完全绑定在了一起,使得代码的可扩展性下降。这是编程的一个大忌:无继承关系的类和类之间必须是低耦合的(即没有继承关系的类和类之间尽量减少直接的联系)。高耦合表现为,当开关保存一个灯对象的引用后,意味着这个开关只能控制灯,触发灯的事件,而无法触发电风扇的事件。这和客观现实不符。
Java语言采用了接口的方法来实现事件,即定义一个具有Open方法的Openable接口,实现这个接口,在Open方法中打开灯或打开电风扇。开关对象保存Openable接口对象的引用。这种方法减少了类和类之间的耦合,是一种不错的方法。
C#引入了委托概念,就像前面代码中描述的一样,C#用一种专门的类型来处理事件触发,这一点比使用接口的Java在语义上更加明确,在使用上也更加方便。而且C#的委托实现了统一建模语言2.0版本中对于事件的描述,更符合软件工程的要求。
事件的多播
委托类型的变量引用到委托实现方法上,并利用属性来对外暴露委托(第21行和第31-38行)。这种方式没有任何问题,只是没有完全体现事件的特色。
在客观世界中,一个开关可以开启无数盏灯,或开启无数盏电风扇,或同时开启灯和风扇。即:事件应该具有同时出发多个对象行为的能力。这种能力称为事件的多播。
event关键字赋予一个委托类型多播的能力,使用event关键字修饰的委托变量,具有多播和触发事件的功能。
代码如下:
Fan.cs
- using System;
- namespace Edu.Study.Graphics.Event {
- /// <summary>
- /// 风扇类
- /// </summary>
- public class Fan {
- /// <summary>
- /// 风扇类型字段
- /// </summary>
- private string fanType;
- /// <summary>
- /// 构造器,构造一个有类型的风扇
- /// </summary>
- /// <param name="fanType">风扇类型</param>
- public Fan(string fanType) {
- this.fanType = fanType;
- }
- /// <summary>
- /// 风扇类型属性,只读
- /// </summary>
- public string FanType {
- get {
- return this.fanType;
- }
- }
- /// <summary>
- /// 风扇工作方法。
- /// </summary>
- /// <param name="status">开关状态</param>
- public void Work(bool status) {
- // 根据传入的状态值确定风扇是转动还是停止
- if (status == true) {
- Console.WriteLine("{0}转动了!", this.FanType);
- } else {
- Console.WriteLine("{0}停止了!", this.FanType);
- }
- }
- }
- }
Light.cs
- using System;
- namespace Edu.Study.Graphics.Event {
- /// <summary>
- /// 定义灯类
- /// </summary>
- public class Light {
- /// <summary>
- /// 定义灯的名字
- /// </summary>
- private string name;
- /// <summary>
- /// 构造器。
- /// </summary>
- /// <param name="name">灯的名字</param>
- public Light(string name) {
- this.Name = name;
- }
- /// <summary>
- /// 灯名字属性
- /// </summary>
- public string Name {
- get {
- return name;
- }
- set {
- name = value;
- }
- }
- /// <summary>
- /// 灯打开方法
- /// </summary>
- /// <param name="status">开关状态</param>
- public void Open(bool status) {
- if (status) {
- Console.WriteLine("{0}亮了。", this.Name);
- } else {
- Console.WriteLine("{0}熄了。", this.Name);
- }
- }
- }
- }
Swticher.cs
- using System;
- namespace Edu.Study.Graphics.Event {
- /// <summary>
- /// 定义返回类型为void,具有一个布尔类型参数的委托类型
- /// </summary>
- /// <param name="status">状态值</param>
- public delegate void EventHandler(bool status);
- /// <summary>
- /// 开关类
- /// </summary>
- public class Switcher {
- /// <summary>
- /// 定义Click事件, 注意这里对event关键字的使用
- /// </summary>
- public event EventHandler Click = null;
- /// <summary>
- /// 定义开关状态,初始为“关”
- /// </summary>
- private bool status = false;
- /// <summary>
- /// 开关按下方法
- /// </summary>
- public void OnClick() {
- this.status = !this.status;
- if (this.Click != null) {
- this.Click(this.status);
- }
- }
- }
- }
Program.cs
- using System;
- namespace Edu.Study.Graphics.Event {
- class Program {
- static void Main(string[] args) {
- // 创建灯、风扇和开关实例
- Light light = new Light("吸顶灯");
- Fan fan = new Fan("换气扇");
- Switcher switcher = new Switcher();
- // 将灯的Open方法和风扇的Work方法关联到开关的Click事件上
- switcher.Click += new EventHandler(light.Open);
- switcher.Click += new EventHandler(fan.Work);
- // 点击开关
- switcher.OnClick();
- switcher.OnClick();
- }
- }
- }
开关类具备一个Click事件和一个OnClick方法(一般将在内部调用事件的方法称为OnXXX方法)。Click事件是一个使用event关键字修饰的委托类型,可以引用到所有返回类型为void,具有一个布尔类型参数的方法上,并通过其来调用方法。event关键字使得这个委托字段支持多播,可以使用+=运算符将多个符合委托的方法关联到该变量上。当OnClick在内部使用该委托变量时,所有关联在该委托变量上的方法都会被调用一次。
有了event修饰,就不用再为委托字段定义属性了,直接使用public修饰符将event修饰的委托类型字段公开即可。
使用事件的一般流程为:
1. 定义委托类型(例如EventHandler委托类型);
2. 使用event关键字修饰并声明委托类型字段(例如public event EventHandler Click);
3. 声明一个名为OnXXX的方法(例如OnClick)在方法内部触发委托定义的事件;
4. 在调用OnClick方法前,使用+=运算符为委托字段赋予合适的方法引用;
5. 调用OnXXX法,触发事件。