事件的由来
前面的三个方法不知道大家都还记得吗,那三个方法都定义在Program类中,这样做是为了方便理解,在实际应用中,通常都是GreetPEople在一个类中,ChineseGreeting和EnglishGreeting在另外一个类中.现在大家已经对委托有了一定得认识,所以我想对上面的例子进行一下改进.结社将GreetPeople()放在一个叫做GreetingManager的类中,那么新程序应该是这样的:
namespace 事件的由来
{
public delegate void GreetingDelegate(string name);
public class GreetingManager
{
public void GreetPeople(string name, GreetingDelegate MakeGreeting)
{
MakeGreeting(name);
}
}
class Program
{
static void Main(string[] args)
{
GreetingManager gm = new GreetingManager();
gm.GreetPeople("张三",ChineseGreeting);
gm.GreetPeople("Jimmy",EnglishGreeting);
}
private static void EnglishGreeting(string name)
{
Console.WriteLine("hello "+name);
}
private static void ChineseGreeting(string name)
{
Console.WriteLine("你好 "+name);
}
}
}
运行这段代码,不会有问题,现在,假设需要使用上一次讲的知识,将多个方法绑定到同一个委托变量,该如何做呢?
static void Main(string[] args)
{
GreetingManager gm = new GreetingManager();
GreetingDelegate delegate1;
delegate1 = EnglishGreeting;
delegate1 += ChineseGreeting;
gm.GreetPeople("Jimmy",delegate1);
}
输出依然是没问题,不禁想到:面向对象设计讲究的是对象的封装,既然可以声明委托类型的变量(上例中是delegate1),那么为何不讲这个变量封装到GreetingManager类中呢?这样就可以在GreetingManager类的客户端中直接使用了,这样不是更方便吗,于是,像这样改写GreetingManager类:
public class GreetingManager
{
//在GreetingManager类的内部声明delegate1变量
public GreetingDelegate delegate1;
public void GreetPeople(string name, GreetingDelegate MakeGreeting)
{
MakeGreeting(name);
}
}
现在,就可以像下面这样使用这个委托变量:
static void Main(string[] args)
{
GreetingManager gm = new GreetingManager();
gm.delegate1 = EnglishGreeting;
gm.delegate1 += ChineseGreeting;
gm.GreetPeople("Jimmy",gm.delegate1);
}
虽然这样没问题,但是你可能觉得这条语句很奇怪.在调用gm.GreetPeople方法的时候,再次传递了gm的delegate1字段:
gm.GreetPeople(“Jimmy”,gm.delegate1);
因为delegate1本身就包含在了gm中,所以可以在进行一下改进,将GreetingManager类修改成这样:
public class GreetingManager
{
//在GreetingManager类的内部声明delegate1变量
public GreetingDelegate delegate1;
public void GreetPeople(string name)
{
if (delegate1!=null)
{
delegate1(name);
}
}
}
在客户端,调用看上去更简洁一下:
static void Main(string[] args)
{
GreetingManager gm = new GreetingManager();
gm.delegate1 = EnglishGreeting;
gm.delegate1 += ChineseGreeting;
gm.GreetPeople("Jimmy");
}
尽管这样达到了想要的结果,但是还是存在问题.在这里,delegate1和平时用的string类型的变量并没有什么分别.大家知道,并不是所有的字段都应该声明为public,合适的做法是应该声明为public的时候声明为public,该private的时候声明为private.
先看看如果把delegate1声明为private会怎样.结果就行:delegate1变得毫无意义.声明委托的目的就是为了把它暴露在类的客户端进行方法注册,把它声明为private了,客户端对它根本就不可见,那它还有什么用?
再看看把delegate1声明为public会怎样.结果就是:在客户端可以对它进行随意的赋值等操作,破坏了对象的封装性.这就好像类中包含一个字段string name时,如果需要对它进行访问,通常并不会直接将name字段声明为public,而是添加一个公共属性public string Name{ get { return name; } }.最后,第一个方法注册用的是”=”,是赋值语法,因为要进行实例化,第二个方法注册用的是”+=”.但是,不管是赋值还是注册,都是将方法绑定到委托上,除了调用时的先后顺序不同,再没有任何的分别,这样使用起来不是很别扭,缺乏一致性吗?
于是,event出场了,它封装了委托类型的变量,相当于为委托类型量身定做的属性(Property).使得:在类的内部,不管声明它是public还是protected,他总是private.在类的外部,注册”+=”和注销”-=”的访问限定符与声明事件时使用的访问限定符相同.再次改写GreetingManager类,将它修改成下面这样:
public class GreetingManager
{
//在GreetingManager类的内部声明delegate1变量
public event GreetingDelegate MakeGreet;
public void GreetPeople(string name)
{
MakeGreet(name);
}
}
可以看到:MakeGreet时间的声明与之前委托变量delegate1的声明唯一的区别是多了个event关键字.看到这里,再结合前面的讲解,应该明白到:事件其实没啥不好理解的,声明一个时间不过类似于声明一个进行了封装的委托类型的变量而已.
为了证明前面的推论,如果想下面这样改写main方法:
static void Main(string[] args)
{
GreetingManager gm = new GreetingManager();
gm.MakeGreet = EnglishGreeting;//编译错误,使用”+=”就可以了
gm.MakeGreet += ChineseGreeting;
gm.GreetPeople("Jimmy");
}
限制类型能力
使用事件不仅能获得比委托更好的封装性,还能限制含有事件的类型的能力.这是什么意思呢?他的意思是说:事件应该由时间的发布者出发,而不应该由事件的客户端(客户程序)来触发.看下面的实例:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace 事件的由来
{
//定义委托
public delegate void NumberChangedEventHandler(int count);
//定义事件发布者
public class Publishser
{
private int count;
public NumberChangedEventHandler NumberChanged;//声明委托变量
//声明一个事件
//public event NumberChangedEventHandler NumberChanged;
public void DoSomething()
{
Console.WriteLine("hello,world");
if (NumberChanged!=null)
{
count++;
NumberChanged(count);
}
}
}
//定义事件的订阅者
public class Subscriber
{
public void OnNumberChanged(int count)
{
Console.WriteLine("Subscriber notified: count = {0}",count);
}
}
class Program
{
static void Main(string[] args)
{
Publishser pub = new Publishser();
Subscriber sub = new Subscriber();
pub.NumberChanged += new NumberChangedEventHandler(sub.OnNumberChanged);
pub.DoSomething();
pub.NumberChanged(100);
Console.Read();
}
}
}
上面代码定义了一个NumberChangedEventHandler委托,然后创建了事件的发布者Publisher和订阅者Subscriber.当使用委托变量时,客户端可以直接通过委托变量触发事件,也就是直接调用pub.NumberChanged(100)这将会影响所有注册了该委托的订阅者.而事件的本意应该为事件发布者在其本身的某个行为中触发,比如在方法DoSomething()中满足某个条件后触发.通过添加event关键字来发布事件,时间发布者的封装性会更好,事件仅仅供其他类型订阅,而客户端不能直接触发事件(语句pub.NumberChanged(100)无法通过编译),事件只能在事件发布者Publisher类得内部触发(比如在方法pub.DoSomething()中),换言之,就是NumberChanged(100)语句只能在Publisher内部被调用.
大家可以尝试一下,将委托变量的声明哪行代码注释掉,然后取消下面事件声明的注释.此时程序是无法编译的,在使用了event关键字之后,直接在客户端出发时间这种行为,也就是直接调用pub.NumberChanged(100)是被禁止的.事件只能通过调用DoSomething()来触发.这样才是事件的本意,事件发布者的封装才会更好.
就如如果要定义一个数字类型,我们会使用int而不是object一样,给对象过多的能力并不是一件好事,应该是越合适越好.尽管直接使用委托变量通常不会有什么问题,但他给了客户端不应具有的能力,而是用时间,可以限制这一能力,更精确的对类型进行封装.
说明一下:这里还有一个约定俗成的规定,就是订阅事件的方法的命名,通常为”On事件名”,比如这里的OnNumberChanged.