引言
本文仅针对C#有一定基础,使用过C#中的委托与事件,对大致的概念和语法有所了解,但是没有很好理解委托与事件的用法及其内部的一些运作规律的同学们,希望用比较生动的举例和浅显的语言,为大家揭开C#委托与事件的面纱。才疏学浅,在博客园的第一盘文章,大家见笑了~
为什么要有委托和事件
但凡C#学习了一段时间,能自己独立编写一些小程序甚至小项目的同学,在遇到委托和事件的时候,几乎都会产生这样的疑问:我为什么要用委托,当我需要调用某个方法的时候,直接调用就好了啊,何必绕一个弯再去委托别人来代替我执行呢?并且整个程序都在我的掌控之下,什么时候要执行什么方法,我大可以自己写一个判断,当条件达成时就执行xxx代码,也用不着别人来通知我啊,要事件何用?于是有了以下的故事:
小明今年毕业并且找到了一份好工作,他决定给自己盖一个小屋庆祝一下~于是小明自己画了一个设计草图,一砖一瓦自己盖了起来:
public void BuildHouse() { for (int day = 0; day < 100; day++) { 小明.Construct(); } Console.WriteLine("屋子盖好啦~"); }
当小屋终于盖好,小明很是得意,原来盖房子也不过如此嘛,自己亲力亲为,不需要别人来指手画脚的。
过了2个月,小明不幸中了500万大奖,瞬间屌丝变身高富帅,于是小明决定给自己换一套大别墅。但是问题来了,别墅是大工程啊,小明自己一个人可做不了,于是乎小明决定情人来帮忙,并且为了发挥咱程序员的伟大创造力,小明决定自己兼任项目经理和设计师。第二天,施工队来了,可小明又犯愁了,我和施工队互不相识,也不了解,该如何配合呢?施工队就说了,没事,你只管把施工图画给我们,具体怎么做由我们来进行。于是就有了委托:
delegate void ConstructDel(); class Program { static void Main(string[] args) { Worker worker = new Worker(); worker.Construct(xiaoming.DrawConstructMap); } } static class xiaoming { public static void DrawConstructMap(); } class Worker { public void Construct(ConstructDel process); }
(备注:小明不了解施工队的内部是怎么进行施工的,但只要小明给施工队画好施工图,剩下的就由施工队去干了。同样,施工队在产生的时候同样不知道小明会有怎样的需求,他们本身就会施工,但是需要小明提供“指导”才能进行工作,这样2个类可以独立设计,互不影响)
然而好景不长,没过多久,小明觉得自己快要累死了,因为这么大一个工程,不是一个施工队就能做完的,要有很多分包的队伍比如:结构施工队、管道施工队、粉刷施工队、装饰装修施工队等等。。。并且这些施工队之间也互不相识,小明需要没日没夜的在现场关注各个工序的施工进度,一旦某个工序完成,要通知别的施工队进行下一个工序。于是小明想了一个办法,让每个施工队完成一道工序后自动通知别的施工队,各个施工环节中,工序内部的所有事情由施工队自行解决,大家唯一关心的问题就是别的施工队的施工进度,因此只要一个施工队完成了自己的工序,并且告之其他人,那么其他施工队就能自行展开下一步工作,于是便有了事件:
delegate void ConstructDel(); class Program { static void Main(string[] args) { Worker w1 = new Worker(); Worker w2 = new Worker(); Worker w3 = new Worker(); w1.ConstructEvent += w2.FollowUp; w1.ConstructEvent += w3.FollowUp; w2.ConstructEvent += w3.FollowUp; w2.ConstructEvent += w1.FollowUp; w3.ConstructEvent += w1.FollowUp; w3.ConstructEvent += w2.FollowUp; w1.IsWorkDone = true; //一旦w1的isWorkDone被设置为true,则触发w1的ConstructEvent事件,w2和w3的FollowUp方法将被执行 .... } } class Worker { bool isWorkDone = false; public bool IsWorkDone { get { return isWorkDone; } set { isWorkDone = value; if (isWorkDone == true) //如果工序完成 { ConstructEvent.Invoke(); //触发事件 } } } public void Construct(ConstructDel process); public event ConstructDel ConstructEvent; public void FollowUp(); //跟进后续工作 }
最后小明又雇了一个设计师来帮忙画图,于是小明终于解脱了,每日只需在办公室喝着咖啡,敲敲代码,写个博客,岂不乐哉。就等着自己梦寐以求的豪宅竣工啦~
(ps:以上例子仅供读者对委托和事件的产生有一个大致的印象,代码较抽象,没有实际意义,如有不甚了解,大可直接跳过,继续往下看,后面会有更实际的举例说明:)
深入了解委托和事件
好吧,接下来是一段枯燥的概念,为了能更深入的理解委托和事件,我们是必须要对委托与事件深层概念进行了解的。
如图,我们可以看到,委托的本质就是一个类,ps:括号中的内容可以跳过;-) [他继承至多播委托类,多播委托又继承至委托基类。委托有3个invoke的方法,我们只要知道invoke就是调用的意思,我们平时在调用绑定到委托或者事件上的方法时,实际上就是调用了invoke方法,我们通过+=绑定方法实际上是MulticastDelegate父类中的一个CombineImpl(Delegate follow)的方法,将我们的方法绑定到了一个委托链表invocationList上,最终执行的时候,程序会遍历这个链表,并依次调用链表上的方法]。委托中保存着指向方法的内存地址,他类似C++中的指针,但是我们C#的委托是面向对象的,类型安全的指针,可以放心大胆的用。
再看事件,这里的FuncEvent在编译的时候自动生成了一个私有的同名委托对象,所以说,事件的本质就是委托!同时还有1个add方法和1个Remove方法,也就是说,对于事件我们只能进行+=操作或者-=操作。
那么既然事件本质就是委托,事件的存在又有什么特殊意义呢?这与字段和属性的关系非常类似,我们通过属性对字段进行封装,在外界只能通过属性来操作,同时我们还可以设置属性的访问类型为只读或只写。同样的,事件也是对委托对象进行了封装,因为委托对象可以使用"="操作,这回覆盖掉委托链表中原有的方法,我们不希望外界能随意操作委托,于是进行了封装,让使用者只能"+="或者"-=",这样就限制了使用者只能添加或移除他已知的方法,对于委托对象内部他不知道的部分进行操作限制。所以通常的用法是:委托在方法的参数中定义,作为回调函数使用。而事件则是在类中直接公开给使用者。
委托示例
接下来我们看一个.net中很常见的用到委托的地方。那就是List<>的sort方法。如图:
sort方法有一个重载,这里的Comparison其实就是一个委托,看:这是这个委托类型的定义。为什么这里会用到委托呢?我相信很多初学c#的同学是没用过这个重载的。因为List<>是泛型,可以存储任何类型的对象,那么这个类要提供一个排序的方法,就会头大了。如果是int double这些类型还好说,但如果是string或者自定义类型,该以什么规则来排序呢?设计List<>这个类的人永远不知道你会往里面存什么样的对象,为了设计一个通用的排序方法,于是就用到了委托,需要使用写一个方法,来告诉我排序规则,然后我才能帮你完成排序(从这里我们可以发现,委托其实并不像字面上那样:我委托一个人来帮我做事情。而是:我请人帮我做事情,这个人就是专门做这事的,他可以为很多人做事,但是他并不清楚你的需求,于是他"委托"你告诉他具体的规则,然后他才去帮你做事)这就好比第一个例子中,施工队就是做施工的,他可以为很多业主干活,但是他不能盲目的就做,需要你把具体实施的办法告诉他,也就是施工图,这样他才能帮你干活。
我们来看排序的示例:
class Program { static void Main(string[] args) { List<Person> list = new List<Person>(); Person p1 = new Person(10, 1, "张三"); Person p2 = new Person(15, 2, "李四"); Person p3 = new Person(5, 3, "王五"); Person p4 = new Person(12, 4, "赵六"); Person[] ps = { p1, p2, p3, p4 }; list.AddRange(ps); Comparison<Person> comparison = new Comparison<Person>(Compare); // list.Sort(comparison); // 这2步可以合并写成list.Sort(Compare); foreach (Person item in list) { Console.WriteLine(item.Name+item.Age); } Console.ReadKey(); } private static int Compare(Person a, Person b) //定义一个满足Comparison委托格式的方法 { return a.Age - b.Age; //告诉他我需要通过年龄进行排序 } } class Person { public int Age; public int Id; public string Name; public Person(int age,int id,string name) { Age = age; Id = id; Name = name; } }
结果如上,这样,我们随便定义一个类,只要设定好规则,都可以通过Sort方法来排序,至于他内部是如何遍历集合,如何排序,是用冒泡排序法还是递归排序我都不用操心。而Sort方法在设计的时候也根本不用考虑如何为2个未知类型的对象进行比较。既实现了通用性,又实现了松耦合。
事件示例
好的,小明又来了。我们的小明是典型的IT男+文艺青年,爱时尚,喜欢网购。而网购呢首选京东,为什么呢?因为物流速度那叫快啊,特别是各大IT园区倍受关照,上午买下午到妥妥的~(不是广告哈:-P),我们知道京东有个降价通知功能,现在我们就来用事件简单模拟一下这个功能~
事件是什么?事件就是我对你感兴趣,我就粉你了,当你的微博更新的时候,系统就会把消息推送给我。。。巴拉巴拉,大概就是这个意思,小明对京东的某个产品感兴趣了,但是价格好像有点高诶,如果再便宜点我就买咯,所以,小明最关注的就是该商品的价格,于是降价通知顺势推出,一旦你订阅了该商品的降价通知,当商品价格降低的时候,系统就会发消息通知你,这就是事件了。
我们来看看京东类怎么设计:
class Jingdong { private int price = 5000; public int Price { get { return price; } set { if (value < price) //当价格被设置,并且设置的值小于当前价格 { PriceEvent.Invoke(); //触发事件 }
price = value;
} } public event PriceDel PriceEvent; }
假设有一个商品原来是5000元,我们在他的price属性中的set方法里进行了判断,一旦商品价格被重新赋值,也就是set方法开始执行,就进行判断,如果设置的新价格小于原价格就触发事件。
用户类:
class User { public User(string name) { this.name = name; } private string name; public string Name { get { return name; } set { name = value; } } public void Alert() { Console.WriteLine("{0}:亲,您关注的商品降价了哦,快来买哦亲~",name); //用户类里有一个用于处理事件的方法,也就是系统通知 } }
最终的运行代码:
class Program { static void Main(string[] args) { Jingdong item = new Jingdong(); User u1 = new User("小明"); User u2 = new User("小黄"); User u3 = new User("小兰"); item.PriceEvent += u1.Alert; item.PriceEvent += u2.Alert; item.PriceEvent += u3.Alert; Console.WriteLine("京东的产品经理更改了商品价格:"); item.Price = Convert.ToInt32(Console.ReadLine()); Console.WriteLine("产品卖光咯~"); Console.ReadKey(); } }
小明、小黄、小兰3个人都对该商品感兴趣,于是都添加了对该商品的降价通知的订阅。当产品经理调整的价格大于原价时:
没有触发任何事件,当调整的价格小于5000时:
看,降价通知发出来了。可是好像还有些缺陷,我们关注的是商品的价格,但是系统通知里并没有包含价格的具体信息,只告诉我们降价了,那么降1块钱也是降,降200也是降啊,并且是什么产品降价了呢,我们也许订阅了很多个感兴趣的产品,得告诉我们具体是哪个产品降价了啊。这么重要的信息,上面的代码都无法提供给我们,这样的通知,说白了就是欠揍:-O 我们再看看代码,是不是和我们平时见到的事件不太一样呢?我们在winform和asp.net中大量用到了事件,当然,多是系统预定事件。这些事件有个普遍的特点,就是有一个object sender参数和一个EventArgs e参数,这2个参数有什么用呢?顾名思义,sender就是发送者,也就是谁给我们发送了通知这个sender就代表谁。而EventArgs翻译过来就是事件参数,也就是我们感兴趣的东西就在这里面了。默认的EventArgs是基类,直接继承与object,里边不包含任何事件信息,当我们需要往事件中添加自定义信息的时候,就要写一个类继承与EventArgs。于是我们把上面的例子重新改写一下:
delegate void PriceDelegate(object sender,PriceEventArgs e); class Program { static void Main(string[] args) { JingdongItem item = new JingdongItem(); Customer u1 = new Customer("张三"); Customer u2 = new Customer("李四"); Customer u3 = new Customer("王五"); item.DiscountEvent += u1.Alert; item.DiscountEvent += u2.Alert; item.DiscountEvent += u3.Alert; Console.WriteLine("京东的产品经理更改了商品价格:"); item.Price = Convert.ToInt32(Console.ReadLine()); Console.WriteLine("产品卖光咯~"); Console.ReadKey(); } } class JingdongItem { public event PriceDelegate DiscountEvent; PriceEventArgs e = new PriceEventArgs(); private string name = "New Pad"; private int price = 3000; public int Price { get { return price; } set { if (value < price) { //给事件参数赋值 e.OrgPrice = price; e.NewPrice = value; e.ItemName = name; price = value; DiscountEvent.Invoke(this, e); } else { price = value; } } } } class Customer { public Customer(string name) { this.name = name; } private string name; public string Name { get { return name; } set { name = value; } } public void Alert(object sender, PriceEventArgs e) { Console.WriteLine("{0}:亲,您关注的商品{1},价格从{2}降到了{3}哦,快来买哦亲~", name,e.ItemName,e.OrgPrice,e.NewPrice); } } class PriceEventArgs : EventArgs //自定义的PriceEventArgs类,继承自EventArgs { private int orgPrice; //原价格 public int OrgPrice { get { return orgPrice; } set { orgPrice = value; } } private int newPrice; //新价格 public int NewPrice { get { return newPrice; } set { newPrice = value; } } private string itemName; //商品名称 public string ItemName { get { return itemName; } set { itemName = value; } } }
运行结果如上,有了事件参数,我们就可以把任何用户关注的信息传递给用户了。但是本例没有使用object sender参数,因为我有更适合演示它的例子,那就是,计算器。假设我们有如下的一个计算器界面:
我们没必要每个按钮都添加一个事件,我们可以把0~9的数字按钮添加到同一个事件,然后通过object sender参数,获得是谁触发的click事件,然后通过把sender转换成Button类,根据其Text属性,就得到了这个数字按键的数字,然后通过一个输出方法把数字输出到文本框中,代码如下:
private void btnNum_Click(object sender, EventArgs e) { Button btn = sender as Button; ShowNum(btn.Text); }
非常的简单且实用。
在实际使用中,我们会大量的用到sender和e,他给我们提供了很大的方便,这也是.net中的观察者模式的应用。
事件在根本上和委托是一致的,都是松耦合的体现,消费订阅了京东的降价通知,就无需自己盯着屏幕按F5,一旦产品价格降低,系统会自动给我们发送消息。而产品本身,也根本无需关心谁订阅自己,它只管在价格变化的时候发出通知即可。委托和事件都是体现在封装上的,自己做一个简单的程序,从a到b,再从b到c一个循序渐进的过程进行开发,是根本没有必要非弄一个委托或者事件来忽悠自己的。而实际工作中,很多时候我们在做a的时候,已经就有了b和c,这时候b和c可能就会预留出一些供我们调用的委托和事件,如果我们的a最后开发完成后是给d使用的,而d又还没开始做,那么我们设计a的时候就要考虑到通用性了。这里其实委托和接口非常的类似,有一个非常言简意赅的说法,和大家分享一下:委托封装了方法,接口封装了类。
总结
.net中的委托与事件是新人很容易晕的部分,我自己也是这么晕过来的:-p 现在也不能说有什么高深的见解,只是希望通过几个简单的例子,和大家分享一下点点见解,让大家一同进步,一同从用别人的委托和事件,到自己写委托、事件进行封装慢慢成长。本文如有论述不对之处,欢迎指正~