文章部分转自:
http://www.cnblogs.com/JimmyZhang/archive/2007/09/23/903360.html 原作者:Jimmy Zhang
http://www.cnblogs.com/yubinfeng/p/4579664.html 原作者: 尼古拉斯*Yu
文章部分内容有删改。
我在和尼古拉斯*Yu讨论委托和事件的时候,他面向天空45°,淡淡的吸了一口烟,轻轻的对我说:
委托嘛,就是你想去干什么,又不想(能)去,所以,你让谁去帮你做。
事件呢,就是一个特殊的(经过封装的)委托。
事情的发展跟导演的一样,很多“鸡汤”文中的经典,年度十佳CSharp面试题,论“程序员”的自我修养。(实际上,尼古拉斯*Yu大 神是一位生产力极高的大神,前面的系列文档,都是大神在一个月内搞定的,其中不乏凌晨,细心的朋友可能也注意到了,简直就是老板最爱啊。看了他的委托的文 章,没有Jimmy Zhang的细腻,但是比他的更深入。技术人的通病~~~),好了言归正传,说说到底什么是委托和事件,为什么要用它。
第I部分: 委托
1>.到底什么是委托
委托是一个类,它定义了方法的类型,使得可以将方法当作另一个方法的参数来进行传递,这种将方法动态地赋给参数的做法,可以避免在程序中大量使用If-Else(Switch)语句,同时使得程序具有更好的可扩展性。
委托(delegate),有些书上叫代理或代表,都是一个意思,为了避免了另一个概念代理(Proxy)混淆,还是叫委托更好一些。
学过c++的人很熟悉指针,C#中没有了指针,使用了委托,不同的是,委托是一个安全的类型,也是面向对象的。
平时我们会遇到这样的例子需要处理,比如有一个动物园(Zoo)(我还是以前面的动物来说吧)里面有狗(Dog)、鸡(Chicken)、羊(Sheep)……,也许还会再进来一些新品种。参观动物员的人想听动物叫声,那么可以让管理员协助(动物只听懂管理员的),这样就是一个委托的例子。
2. 委托的使用
委托(delegate)的声明的语法如下:
public delegate void Del(string parameter);
定义委托基本上是定义一个新类,所以可以在定义类的任何地方定义委托,既可以在另一个类的内部定义,也可以在任何类的外部定义,还可以在命名空间中把委托定义为顶层对象。根据定义的可见性,可以在委托定义上添加一般的访问修饰符:public、private、protected等:
实际上,“定义一个委托”是指“定义一个新类”。只是把class换成了delegate而已,委托实现为派生自基类System. Multicast Delegate的类,System.MulticastDelegate又派生自基类System.Delegate。
下面我们使用委托来实现上面动物园的实例,实现如下:
1 /// <summary>
2 /// 动物类
3 /// </summary>
4 class Zoo
5 {
6 public class Manage
7 {
8 public delegate void Shout();
9 public static void CallAnimalShout(Shout shout)
10 {
11 shout();
12 }
13 }
14 public class Dog
15 {
16 string name;
17 public Dog(string name)
18 {
19 this.name = name;
20 }
21 public void DogShout() {
22
23 Console.WriteLine("我是小狗:" + this.name + "汪~汪~汪");
24 }
25 }
26 public class Sheep
27 {
28 string name;
29 public Sheep(string name)
30 {
31 this.name = name;
32 }
33 public void SheepShout()
34 {
35 Console.WriteLine("我是小羊:" + this.name + "咩~咩~咩");
36 }
37 }
38 public class Checken
39 {
40 string name;
41 public Checken(string name)
42 {
43 this.name = name;
44 }
45 public void ChickenShout()
46 {
47 Console.WriteLine("我是小鸡:" + this.name + "喔~喔~喔");
48 }
49 }
50 }
动物园除了各种动物外,还有动物管理员,动物管理员有一个委托。调用如下:
//参观者委托管理员,让某种动物叫
Zoo.Dog dog=new Zoo.Dog("汪财");
Zoo.Manage.Shout shout = new Zoo.Manage.Shout(dog.DogShout);
//管理员收到委托传达给动物,动物执行主人命令
Zoo.Manage.CallAnimalShout(shout);
运行结果如下:
上面的实例实现了委托的定义和调用,即 间接的调用了动物叫的方法。肯定有人会说,为什么不直接调用小狗叫的方法,而要绕一大圈来使用委托。如果只是简单的让一种动物叫一下,那么用委托确实是绕 了一大圈,但是如果我让让狗叫完,再让羊叫,再让鸡叫,反反复复要了好几种动物的叫声,最后到如果要结算费用,谁能知道我消费了多少呢?如果一次让几种动 物同时叫呢,我们是不是要再写一个多个动物叫的方法来调用呢?当遇到复杂的调用时委托的作用就体现出来了,下面我们先看一下,如何让多个动物同时叫,就是 下面要说的多播委托。
委托需要满足4个条件:
a.声明一个委托类型
b.找到一个跟委托类型具有相同签名的方法(可以是实例方法,也可以是静态方法)
c.通过相同签名的方法来创建一个委托实例
c.通过委托实例的调用完成对方法的调用
3. 多播委托
每个委托都只包含一个方法调用,调用委托的次数与调用方法的次数相同。如果调用多个方法,就需要多次显示调用这个委托。当然委托也可以包含多个方法,这种委托称为多播委托。
当调用多播委托时,它连续调用每个方法。在调用过程中,委托必须为同类型,返回类型一般为void,这样才能将委托的单个实例合并为一个多播委托。如果委托具有返回值和/或输出参数,它将返回最后调用的方法的返回值和参数。
下面我们看一下,调用“狗,鸡,羊”同时叫的实现:
//声明委托类型
Zoo.Manage.Shout shout;
//加入狗叫委托
shout = new Zoo.Manage.Shout(new Zoo.Dog("小哈").DogShout);
//加入鸡叫委托
shout += new Zoo.Manage.Shout(new Zoo.Checken("大鹏").ChickenShout);
//加入羊叫委托
shout += new Zoo.Manage.Shout(new Zoo.Sheep("三鹿").SheepShout);
//执行委托
Zoo.Manage.CallAnimalShout(shout);
Console.ReadLine();
运行结果如下:
上面的示例 ,多播委托用+=来添加委托,同样可以使用 -=来移除委托。
上面的示例,如果我们感觉还不足以体现委托的作用。我们假动物除了会叫之外,还有其它特技。狗会表演“捡东西(PickUp)”,羊会踢球(PlayBall),鸡会跳舞(Dance)
观众想看一个集体表演了,让狗叫1次,抢一个东西回来;羊叫1次踢1次球,鸡叫1次跳1只舞。 然后,顺序倒过来再表演一次。如果使用直接调用方法,那么写代码要疯了,顺序执行一次,就顺序写一排方法代码,要反过来表演,又要倒过来写一排方法。这还不算高难度的表演,假如要穿插进行呢?使用委托的面向对象特征,我们实现这些需求很简单。看代码:
首先我们改进一下羊,狗,鸡,让他们有一个特技的方法。
1 /// <summary>
2 /// 动物类
3 /// </summary>
4 class Zoo
5 {
6 public class Manage
7 {
8 public delegate void del();
9
10 /// <summary>
11 /// 动物表演
12 /// </summary>
13 /// <param name="obj"></param>
14 /// <param name="shout"></param>
15 public static void CallAnimal(del d)
16 {
17 d();
18 }
19 }
20 public class Dog
21 {
22 string name;
23 public Dog(string name)
24 {
25 this.name = name;
26 }
27 public void DogShout()
28 {
29 Console.WriteLine("我是小狗:"+this.name+"汪~汪~汪");
30 }
31 public void PickUp()
32 {
33 Console.WriteLine("小狗" + this.name + " 捡东西 回来了");
34 }
35 }
36 public class Sheep
37 {
38 string name;
39 public Sheep(string name)
40 {
41 this.name = name;
42 }
43 public void SheepShout()
44 {
45 Console.WriteLine( "我是小羊:"+this.name+" 咩~咩~咩 ");
46 }
47 public void PlayBall()
48 {
49 Console.WriteLine("小羊" + this.name + " 打球 结束了");
50 }
51 }
52
53 public class Chicken
54 {
55 string name;
56 public Chicken(string name)
57 {
58 this.name = name;
59 }
60 public void ChickenShout()
61 {
62 Console.WriteLine("我是小鸡:"+this.name+"喔~喔~喔");
63 }
64 public void Dance()
65 {
66 Console.WriteLine("小鸡" + this.name + " 跳舞 完毕");
67 }
68 }
69 }
调用如下:
1 //多播委托(二)动物狂欢
2
3 //挑选三个表演的动物
4 Zoo.Dog dog = new Zoo.Dog("小哈");
5 Zoo.Chicken chicken = new Zoo.Chicken("大鹏");
6 Zoo.Sheep sheep = new Zoo.Sheep("三鹿");
7
8 //加入狗叫委托
9 Zoo.Manage.del dogShout = dog.DogShout;
10 //加入鸡叫委托
11 Zoo.Manage.del chickenShout = chicken.ChickenShout;
12 //加入羊叫委托
13 Zoo.Manage.del sheepnShout = sheep.SheepShout;
14
15 //加入狗表演
16 Zoo.Manage.del dogShow = new Zoo.Manage.del(dog.PickUp);
17 //加入鸡表演
18 Zoo.Manage.del chickenShow = new Zoo.Manage.del(chicken.Dance);
19 //加入羊表演
20 Zoo.Manage.del sheepShow = new Zoo.Manage.del(sheep.PlayBall);
21
22
23 //构造表演模式
24 //第一种表演方式:狗叫1次抢一个东西回来;羊叫1次踢1次球;鸡叫1次跳1只舞;
25 Zoo.Manage.del del = dogShout + dogShow + chickenShout + chickenShow + sheepnShout + sheepShow;
26 //执行委托
27 Zoo.Manage.CallAnimal(del);
28
29
30 Console.WriteLine("
第二种表演,顺序反转
");
31 //第二种表演,顺序反转
32 var del2 = del.GetInvocationList().Reverse();
33 //执行委托
34 foreach (Zoo.Manage.del d in del2)
35 Zoo.Manage.CallAnimal(d);
36 Console.ReadLine();
运行结果如下:
使用多播委托有两点要注意的地方:
(1)多播委托的方法并没有明确定义其顺序,尽量避免在对方法顺序特别依赖的时候使用。
(2)多播委托在调用过程中,其中一个方法抛出异常,则整个委托停止。
4. 匿名方法
我们通常都都显式定义了一个方法,以便委托调用,有一种特殊的方法,可以直接定义在委托实例的区块里面。我们在LINQ基础一节中,已经举例说明过匿名方法。实例化普通方法的委托和匿名方法的委托有一点差别。下面我们看一下示例:
//定义委托
delegate void Add(int a,int b);
//实例委托,使用匿名方法
Add add = delegate(int a, int b)
{
Console.WriteLine(a + "+" + b + "=" + (a + b));
};
//调用
add(1, 2);
add(11, 32);
返回结果为: 1+2=3 11+32=43
4.1 对于匿名方法有几点注意:
(1)在匿名方法中不能使用跳转语句调到该匿名方法的外部;反之亦然:匿名方法外部的跳转语句不能调到该匿名方法的内部。
(2)在匿名方法内部不能访问不完全的代码。
(3)不能访问在匿名方法外部使用的ref和out参数,但可以使用在匿名方法外部定义的其他变量。
(4)如果需要用匿名方法多次编写同一个功能,就不要使用匿名方法,而编写一个指定的方法比较好,因为该方法只能编写一次,以后可通过名称引用它。
4.2 匿名方法的适用环境:
(1)在调用上下文中的变量时
(2)该方法只调用一次时,如果方法在外部需要多次调用,建议使用显示定义一个方法.
可见,匿名方法是一个轻量级的写法。
4.3 使用Labmda表达式书写匿名方法
在Linq基础一节中,我们说了,Labmda表达式是基于数学中的λ(希腊第11个字母)演算得名,而“Lambda 表达式”(lambda expression)是指用一种简单的方法书写匿名方法。
上面的匿名方法,我们可以使用等效的Labmda表达式来书写,如下:
//使用Lambda表达式的匿名方法 实例化并调用委托
Add add2 = (a, b) => { Console.WriteLine(a + "+" + b + "=" + (a + b)); };
add2(3, 4);
add2(3, 31);
//返回结果为:3+4=7 3+31=34
“=>”符号左边为表达式的参数列表,右边则是表达式体(body)。参数列表可以包含0到多个参数,参数之间使用逗号分割。
5. 泛型委托
前面我们说了通常情况下委托的声明及使用,除此之外,还有泛型委托
泛型委托一共有三种:
Action(无返回值泛型委托)
Func(有返回值泛型委托)
predicate(返回值为bool型的泛型委托)
下面一一举例说明
5.1 Action(无返回值泛型委托)
示例如下:
1 /// <summary>
2 /// 提供委托签名方法
3 /// </summary>
4 /// <typeparam name="T"></typeparam>
5 /// <param name="action"></param>
6 /// <param name="a"></param>
7 /// <param name="b"></param>
8 static void ActionAdd<T>(Action<T,T> action,T a,T b)
9 {
10 action(a,b);
11 }
12
13 //两个被调用方法
14 static void Add(int a,int b)
15 {
16 Console.WriteLine(a + "+" + b + "=" + (a + b));
17 }
18
19 static void Add(int a, int b,int c)
20 {
21 Console.WriteLine(a + "+" + b + "+"+c+"=" + (a + b));
22 }
声明及调用如下:
//普通方式调用
ActionAdd<int>(Add,1,2);
//匿名方法声明及调用
Action<int,int> acc = delegate(int a,int b){
Console.WriteLine(a + "+" + b + "=" + (a + b));
};
acc(11, 22);
//表达式声明及调用
Action<int, int> ac = (a,b)=>{ Console.WriteLine(a + "+" + b + "=" + (a + b)); };
ac(111, 222);
返回值如下:
可以使用 Action<T1, T2, T3, T4> 委托以参数形式传递方法,而不用显式声明自定义的委托。
封装的方法必须与此委托定义的方法签名相对应。 也就是说,封装的方法必须具有四个均通过值传递给它的参数,并且不能返回值。
(在 C# 中,该方法必须返回 void)通常,这种方法用于执行某个操作。
5.2 Func(有返回值泛型委托)
示例如下:
1 /// <summary>
2 /// 提供委托签名方法
3 /// </summary>
4 /// <typeparam name="T"></typeparam>
5 /// <param name="action"></param>
6 /// <param name="a"></param>
7 /// <param name="b"></param>
8 static string FuncAdd<T,T2>(Func<T,T2,string> func,T a,T2 b)
9 {
10 return func(a,b);
11 }
12
13 //两个被调用方法
14 static string Add(int a,int b)
15 {
16 return (a + "+" + b + "=" + (a + b));
17 }
调用如下:
//有返回值的泛型委托Func
//普通方式调用
Console.WriteLine(FuncAdd<int,int>(Add, 1, 2));
//匿名方法声明及调用
Func<int,int,string> acc = delegate(int a,int b){
return (a + "+" + b + "=" + (a + b));
};
Console.WriteLine(acc(11, 22));
//表达式声明及调用
Func<int, int,string> ac = (a, b) => {return (a + "+" + b + "=" + (a + b)); };
Console.WriteLine(ac(111, 222));
运行结果同上例
5.3 predicate(返回值为bool型的泛型委托)
表示定义一组条件并确定指定对象是否符合这些条件的方法。此委托由 Array 和 List 类的几种方法使用,用于在集合中搜索元素。
使用MSDN官方的示例如下 :
1 //以下示例需要引用System.Drawing程序集
2 private static bool ProductGT10( System.Drawing.Point p)
3 {
4 if (p.X * p.Y > 100000)
5 {
6 return true;
7 }
8 else
9 {
10 return false;
11 }
12 }
调用及运行结果如下:
System.Drawing.Point[] points = { new System.Drawing.Point(100, 200),
new System.Drawing.Point(150, 250), new System.Drawing.Point(250, 375),
new System.Drawing.Point(275, 395), new System.Drawing.Point(295, 450) };
System.Drawing.Point first = Array.Find(points, ProductGT10);
Console.WriteLine("Found: X = {0}, Y = {1}", first.X, first.Y);
Console.ReadKey();
//输出结果为:
//Found: X = 275, Y = 395
6.委托中的协变和逆变
将方法签名与委托类型匹配时,协变和逆变为您提供了一定程度的灵活性。协变允许方法具有的派生返回类型比委托中定义的更多。逆变允许方法具有的派生参数类型比委托类型中的更少
关于协变和逆变要从面向对象继承说起。继承关系是指子类和父类之间的关系;子类从父类继承所以子类的实例也就是父类的实例。比如说Animal是父类,Dog是从Animal继承的子类;如果一个对象的类型是Dog,那么他必然是Animal。
协变逆变正是利用继承关系 对不同参数类型或返回值类型 的委托或者泛型接口之间做转变。我承认这句话很绕,如果你也觉得绕不妨往下看看。
如果一个方法要接受Dog参数,那么另一个接受Animal参数的方法肯定也可以接受这个方法的参数,这是Animal向Dog方向的转变是逆变。 如果一个方法要求的返回值是Animal,那么返回Dog的方法肯定是可以满足其返回值要求的,这是Dog向Animal方向的转变是协变。
由子类向父类方向转变是协变 协变用于返回值类型用out关键字
由父类向子类方向转变是逆变 逆变用于方法的参数类型用in关键字
协变逆变中的协逆是相对于继承关系的继承链方向而言的。
6.1 数组的协变:
Animal[] animalArray = new Dog[]{};
上面一行代码是合法的,声明的数组数据类型是Animal,而实际上赋值时给的是Dog数组;每一个Dog对象都可以安全的转变为Animal。Dog向Animal方法转变是沿着继承链向上转变的所以是协变
6.2 委托中的协变和逆变
6.2.1 委托中的协变
//委托定义的返回值是Animal类型是父类
public delegate Animal GetAnimal();
//委托方法实现中的返回值是Dog,是子类
static Dog GetDog(){return new Dog();}
//GetDog的返回值是Dog, Dog是Animal的子类;返回一个Dog肯定就相当于返回了一个Animal;所以下面对委托的赋值是有效的
GetAnimal getMethod = GetDog;
6.2.2 委托中的逆变
//委托中的定义参数类型是Dog
public delegate void FeedDog(Dog target);
//实际方法中的参数类型是Animal
static void FeedAnimal(Animal target){}
// FeedAnimal是FeedDog委托的有效方法,因为委托接受的参数类型是Dog;而FeedAnimal接受的参数是animal,Dog是可以隐式转变成Animal的,所以委托可以安全的的做类型转换,正确的执行委托方法;
FeedDog feedDogMethod = FeedAnimal;
定义委托时的参数是子类,实际上委托方法的参数是更宽泛的父类Animal,是父类向子类方向转变,是逆变
6.3 泛型委托的协变和逆变:
6.3.1 泛型委托中的逆变
如下委托声明:
public delegate void Feed<in T>(T target)
Feed委托接受一个泛型类型T,注意在泛型的尖括号中有一个in关键字,这个关键字的作用是告诉编译器在对委托赋值时类型T可能要做逆变
/先声明一个T为Animal的委托
Feed<Animal> feedAnimalMethod = a=>Console.WriteLine(“Feed animal lambda”);
//将T为Animal的委托赋值给T为Dog的委托变量,这是合法的,因为在定义泛型委托时有in关键字,如果把in关键字去掉,编译器会认为不合法
Feed<Dog> feedDogMethod = feedAnimalMethod;
6.3.2 泛型委托中的协变
如下委托声明:
public delegate T Find<out T>();
Find委托要返回一个泛型类型T的实例,在泛型的尖括号中有一个out关键字,该关键字表明T类型是可能要做协变的
//声明Find<Dog>委托
Find<Dog> findDog = ()=>new Dog();
//声明Find<Animal>委托,并将findDog赋值给findAnimal是合法的,类型T从Dog向Animal转变是协变
Find<Animal> findAnimal = findDog;
6.4 泛型接口中的协变和逆变:
泛型接口中的协变逆变和泛型委托中的非常类似,只是将泛型定义的尖括号部分换到了接口的定义上。
6.4.1 泛型接口中的逆变
如下接口定义:
public interface IFeedable<in T>
{
void Feed(T t);
}
接口的泛型T之前有一个in关键字,来表明这个泛型接口可能要做逆变
如下泛型类型FeedImp<T>,实现上面的泛型接口;需要注意的是协变和逆变关键字in,out是不能在泛型类中使用的,编译器不允许
public class FeedImp<T>:IFeedable<T>
{
public void Feed(T t){
Console.WriteLine(“Feed Animal”);
}
}
来看一个使用接口逆变的例子:
IFeedable<Dog> feedDog = new FeedImp<Animal>();
上面的代码将FeedImp<Animal>类型赋值给了IFeedable<Dog>的变量;Animal向Dog转变了,所以是逆变
6.4.2 泛型接口中的协变
如下接口的定义:
public interface IFinder<out T>
{
T Find();
}
泛型接口的泛型T之前用了out关键字来说明此接口是可能要做协变的;如下泛型接口实现类
public class Finder<T>:IFinder<T> where T:new()
{
public T Find(){
return new T();
}
}
//使用协变,IFinder的泛型类型是Animal,但是由于有out关键字,我可以将Finder<Dog>赋值给它
Finder<Animal> finder = new Finder<Dog>();
协变和逆变的概念不太容易理解,可以通过实际代码思考理解。这么绕的东西到底有用吗?答案是肯定的,通过协变和逆变可以更好的复用代码。复用是软件开发的一个永恒的追求。
7. 要点
7.1 委托的返回值及参数总结
(1)Delegate至少0个参数,至多32个参数,可以无返回值,也可以指定返回值类型
(2)Func可以接受0个至16个传入参数,必须具有返回值
(3)Action可以接受0个至16个传入参数,无返回值
(4)Predicate只能接受一个传入参数,返回值为bool类型
7.2 委托的几种写法总结:
(1)、委托 委托名=new 委托(会调用的方法名); 委托名(参数);
(2)、委托 委托名 =会调用的方法名; 委托名(参数);
(3)、匿名方法
委托 委托名=delegate(参数){会调用的方法体};委托名(参数);
(4)、拉姆达表达式
委托 委托名=((参数1,。。参数n)=>{会调用的方法体});委托名(参数);
(5)、用Action<T>和Func<T>,第一个无返回值
Func<参数1, 参数2, 返回值> 委托名= ((参数1,参数2) => {带返回值的方法体 });返回值=委托名(参数1,参数2);
7.3.重要的事情说三遍:
(1)“委托”(delegate)(代表、代理):是类型安全的并且完全面向对象的。在C#中,所有的代理都是从System.Delegate类派生的(delegate是System.Delegate的别名)。
(2)委托隐含具有sealed属性,即不能用来派生新的类型。
(3)委托最大的作用就是为类的事件绑定事件处理程序。
(4)在通过委托调用函数前,必须先检查委托是否为空(null),若非空,才能调用函数。
(5)委托理实例中可以封装静态的方法也可以封装实例方法。
(6)在创建委托实例时,需要传递将要映射的方法或其他委托实例以指明委托将要封装的函数原型(.NET中称为方法签名:signature)。注意,如果映射的是静态方法,传递的参数应该是类名.方法名,如果映射的是实例方法,传递的参数应该是实例名.方法名。
(7)只有当两个委托实例所映射的方法以及该方法所属的对象都相同时,才认为它们是相等的(从函数地址考虑)。
(8)多个委托实例可以形成一个委托链,System.Delegate中定义了用来维护委托链的静态方法Combion,Remove,分别向委托链中添加委托实例和删除委托实例。
(9)委托三步曲:
a.生成自定义委托类:delegate int MyDelegate();
b.然后实例化委托类:MyDelegate d = new MyDelegate(MyClass.MyMethod);
c.最后通过实例对象调用方法:int ret = d()
(10)委托的返回值通常是void,虽然不是必须的,但是委托允许定义多个委托方法(即多播委托),设想他们都有返回值,最后返回的值会覆盖前面的,因此通常都定义为void.
第II部分: 事件
委托、事件与Observer设计模式
范例说明
假设我们有个高档的热水器,我们给它通上电,当水温超过95度的时候:1、扬声器会开始发出语音,告诉你水的温度;2、液晶屏也会改变水温的显示,来提示水已经快烧开了。
现在我们需要写个程序来模拟这个烧水的过程,我们将定义一个类来代表热水器,我们管它叫:Heater,它有代表水温的字段,叫做 temperature;当然,还有必不可少的给水加热方法BoilWater(),一个发出语音警报的方法MakeAlert(),一个显示水温的方 法,ShowMsg()。
namespace Delegate {
class Heater {
private int temperature; // 水温
// 烧水
public void BoilWater() {
for (int i = 0; i <= 100; i++) {
temperature = i;
if (temperature > 95) {
MakeAlert(temperature);
ShowMsg(temperature);
}
}
}
// 发出语音警报
private void MakeAlert(int param) {
Console.WriteLine("Alarm:嘀嘀嘀,水已经 {0} 度了:" , param);
}
// 显示水温
private void ShowMsg(int param) {
Console.WriteLine("Display:水快开了,当前温度:{0}度。" , param);
}
}
class Program {
static void Main() {
Heater ht = new Heater();
ht.BoilWater();
}
}
}
Observer设计模式简介
上面的例子显然能完成我们之前描述的工作,但是却并不够好。现在假设热水器由三部分组成:热水器、警报器、显示器,它们来自于不同厂商并进行了组装。那么,应该是热水器仅仅负责烧水,它不能发出警报也不能显示水温;在水烧开时由警报器发出警报、显示器显示提示和水温。
这时候,上面的例子就应该变成这个样子:
// 热水器
public class Heater {
private int temperature;
// 烧水
private void BoilWater() {
for (int i = 0; i <= 100; i++) {
temperature = i;
}
}
}
// 警报器
public class Alarm{
private void MakeAlert(int param) {
Console.WriteLine("Alarm:嘀嘀嘀,水已经 {0} 度了:" , param);
}
}
// 显示器
public class Display{
private void ShowMsg(int param) {
Console.WriteLine("Display:水已烧开,当前温度:{0}度。" , param);
}
}
这里就出现了一个问题:如何在水烧开的时候通知报警器和显示器?在继续进行之前,我们先了解一下Observer设计模式,Observer设计模式中主要包括如下两类对象:
- Subject:监视对象,它往往包含着其他对象所感兴趣的内容。在本范例中,热水器就是一个监视对象,它包含的其他对象所感兴趣的内容,就是temprature字段,当这个字段的值快到100时,会不断把数据发给监视它的对象。
- Observer:监视者,它监视Subject,当Subject中的某件事发生的时候,会告知Observer,而Observer则会采取相应的行动。在本范例中,Observer有警报器和显示器,它们采取的行动分别是发出警报和显示水温。
在本例中,事情发生的顺序应该是这样的:
- 警报器和显示器告诉热水器,它对它的温度比较感兴趣(注册)。
- 热水器知道后保留对警报器和显示器的引用。
- 热水器进行烧水这一动作,当水温超过95度时,通过对警报器和显示器的引用,自动调用警报器的MakeAlert()方法、显示器的ShowMsg()方法。
类似这样的例子是很多的,GOF对它进行了抽象,称为Observer设计模式:Observer设计模式是为了定义对象间的一种一对多的依赖关系,以便于当一个对象的状态改变时,其他依赖于它的对象会被自动告知并更新。Observer模式是一种松耦合的设计模式。
实现范例的Observer设计模式
我们之前已经对委托和事件介绍很多了,现在写代码应该很容易了,现在在这里直接给出代码,并在注释中加以说明。
using System;
using System.Collections.Generic;
using System.Text;
namespace Delegate {
// 热水器
public class Heater {
private int temperature;
public delegate void BoilHandler(int param); //声明委托
public event BoilHandler BoilEvent; //声明事件
// 烧水
public void BoilWater() {
for (int i = 0; i <= 100; i++) {
temperature = i;
if (temperature > 95) {
if (BoilEvent != null) { //如果有对象注册
BoilEvent(temperature); //调用所有注册对象的方法
}
}
}
}
}
// 警报器
public class Alarm {
public void MakeAlert(int param) {
Console.WriteLine("Alarm:嘀嘀嘀,水已经 {0} 度了:", param);
}
}
// 显示器
public class Display {
public static void ShowMsg(int param) { //静态方法
Console.WriteLine("Display:水快烧开了,当前温度:{0}度。", param);
}
}
class Program {
static void Main() {
Heater heater = new Heater();
Alarm alarm = new Alarm();
heater.BoilEvent += alarm.MakeAlert; //注册方法
heater.BoilEvent += (new Alarm()).MakeAlert; //给匿名对象注册方法
heater.BoilEvent += Display.ShowMsg; //注册静态方法
heater.BoilWater(); //烧水,会自动调用注册过对象的方法
}
}
}
输出为:
Alarm:嘀嘀嘀,水已经 96 度了:
Alarm:嘀嘀嘀,水已经 96 度了:
Display:水快烧开了,当前温度:96度。
// 省略...
.Net Framework中的委托与事件
尽管上面的范例很好地完成了我们想要完成的工作,但是我们不仅疑惑:为什么.Net Framework 中的事件模型和上面的不同?为什么有很多的EventArgs参数?
在回答上面的问题之前,我们先搞懂 .Net Framework的编码规范:
- 委托类型的名称都应该以EventHandler结束。
- 委托的原型定义:有一个void返回值,并接受两个输入参数:一个Object 类型,一个 EventArgs类型(或继承自EventArgs)。
- 事件的命名为 委托去掉 EventHandler之后剩余的部分。
- 继承自EventArgs的类型应该以EventArgs结尾。
再做一下说明:
- 委托声明原型中的Object类型的参数代表了Subject,也就是监视对象,在本例中是 Heater(热水器)。回调函数(比如Alarm的MakeAlert)可以通过它访问触发事件的对象(Heater)。
- EventArgs 对象包含了Observer所感兴趣的数据,在本例中是temperature。
上面这些其实不仅仅是为了编码规范而已,这样也使得程序有更大的灵活性。比如说,如果我们不光想获得热水器的温度,还想在Observer端(警报器或者显示器)方法中获得它的生产日期、型号、价格,那么委托和方法的声明都会变得很麻烦,而如果我们将热水器的引用传给警报器的方法,就可以在方法中直接访问热水器了。
现在我们改写之前的范例,让它符合 .Net Framework 的规范:
using System;
using System.Collections.Generic;
using System.Text;
namespace Delegate {
// 热水器
public class Heater {
private int temperature;
public string type = "RealFire 001"; // 添加型号作为演示
public string area = "China Xian"; // 添加产地作为演示
//声明委托
public delegate void BoiledEventHandler(Object sender, BoiledEventArgs e);
public event BoiledEventHandler Boiled; //声明事件
// 定义BoiledEventArgs类,传递给Observer所感兴趣的信息
public class BoiledEventArgs : EventArgs {
public readonly int temperature;
public BoiledEventArgs(int temperature) {
this.temperature = temperature;
}
}
// 可以供继承自 Heater 的类重写,以便继承类拒绝其他对象对它的监视
protected virtual void OnBoiled(BoiledEventArgs e) {
if (Boiled != null) { // 如果有对象注册
Boiled(this, e); // 调用所有注册对象的方法
}
}
// 烧水。
public void BoilWater() {
for (int i = 0; i <= 100; i++) {
temperature = i;
if (temperature > 95) {
//建立BoiledEventArgs 对象。
BoiledEventArgs e = new BoiledEventArgs(temperature);
OnBoiled(e); // 调用 OnBolied方法
}
}
}
}
// 警报器
public class Alarm {
public void MakeAlert(Object sender, Heater.BoiledEventArgs e) {
Heater heater = (Heater)sender; //这里是不是很熟悉呢?
//访问 sender 中的公共字段
Console.WriteLine("Alarm:{0} - {1}: ", heater.area, heater.type);
Console.WriteLine("Alarm: 嘀嘀嘀,水已经 {0} 度了:", e.temperature);
Console.WriteLine();
}
}
// 显示器
public class Display {
public static void ShowMsg(Object sender, Heater.BoiledEventArgs e) { //静态方法
Heater heater = (Heater)sender;
Console.WriteLine("Display:{0} - {1}: ", heater.area, heater.type);
Console.WriteLine("Display:水快烧开了,当前温度:{0}度。", e.temperature);
Console.WriteLine();
}
}
class Program {
static void Main() {
Heater heater = new Heater();
Alarm alarm = new Alarm();
heater.Boiled += alarm.MakeAlert; //注册方法
heater.Boiled += (new Alarm()).MakeAlert; //给匿名对象注册方法
heater.Boiled += new Heater.BoiledEventHandler(alarm.MakeAlert); //也可以这么注册
heater.Boiled += Display.ShowMsg; //注册静态方法
heater.BoilWater(); //烧水,会自动调用注册过对象的方法
}
}
}
输出为:
Alarm:China Xian - RealFire 001:
Alarm: 嘀嘀嘀,水已经 96 度了:
Alarm:China Xian - RealFire 001:
Alarm: 嘀嘀嘀,水已经 96 度了:
Alarm:China Xian - RealFire 001:
Alarm: 嘀嘀嘀,水已经 96 度了:
Display:China Xian - RealFire 001:
Display:水快烧开了,当前温度:96度。
// 省略 ...
通过一个GreetingPeople的小程序向大家介绍了委托的概念、委托用来做什么,随后又引出了事件,接着对委托与事件所产生的中间代码做了粗略的讲述。
在第二个稍微复杂点的热水器的范例中,我向大家简要介绍了 Observer设计模式,并通过实现这个范例完成了该模式,随后讲述了.Net Framework中委托、事件的实现方式。
总结
之前的说过的泛型,其中泛型类和泛型方法的使用场景为了解决一个未知的且变化的参数。现在说到的委托,它的应用场景相对于之前变化的参数,现在直接成了“未知的变化的方法(泛型委托)”,然而事件,保证了封装的完整性,让整个代码更易读易懂。