今天跟随视频学习了一下C#中最重要的一些概念之委托与事件。视频讲的还是挺深入浅出,不过刚接触C#.NET的人还是朦朦胧胧,就像“每次见到委托和事件就觉得心里别(biè)得慌,混身不自在”。跨过这道坎的人就有种一览众山小的感觉了。
1.委托是神马?
用最通俗易懂的话来讲,你就可以把委托看成是用来执行方法(函数)的一个“指针”。用邹老师的一个举例:“设想,如果我们写了一个厨师做菜方法用来做菜,里面有拿菜、切菜、配菜、炒菜 四个环节,但编写此方法代码的人想让配菜这个环节让调用方法的人实现,换句话说,就是想在方法被调用时接收代码 作为参数,在方法中执行这端传进来的代码。但,怎么为一个方法传 代码 进来呢?当然大家想到了传递接口方式来实现,咱先不讨论接口,因为微软为我们提供了一个叫做委托的类型。”
现在来看看怎样使用委托,根据itcast的ppt内容:
声明委托的方式:delegate 返回值类型 委托类型名(参数) 比如delegate void StringProcess(string s); 注意这里的除了前面的delegate,剩下部分和声明一个函数一样,但是StringProcess不是函数名,而是委托类型名
声明的委托是一种类型,就像int、Person一样,如果要用的话还要声明委托类型的变量,声明委托类型变量的方式:StringProcess f1;
将委托类型变量指向函数 StringProcess sp = new StringProcess(SayHello),这样就可以像调用普通函数一样把sp当成函数用了。委托可以看做是函数的指针。整数可以用整数变量指向它,对象可以用对象变量指向它,函数也可以用委托变量指向它。和直接调用函数的区别:用委托就可以指向任意的函数,哪怕是之前没定义的都可以,而不使用受限于那几种。
将委托类型变量指向函数还可以简化成StringProcess sp = SayHello,编译器帮我们进行了new。但是不能sp=PrintIt(),因为这样就成了“执行PrintIt函数,并且将sp指向PrintIt的返回值”。
这里看一个数据过滤的例子,输出int数组中的正整数:
1.声明一个委托:delegate bool FilterDelegate(int i);
2.封装一个过滤的静态方法,参数中包含一个过滤器的方法委托,返回泛型List<int>列表:
static List<int> Filter(List<int> list,FilterDelegate fd) { List<int> listTest = new List<int>(); foreach(int i in list) { if(fd(i)) { listTest.Add(i); } } return listTest; }
3.写一个判断是否为正整数的方法,返回值为bool类型:
static bool isZhengshu(int i) { return i > 0; }
4.在main函数中声明一个List列表,然后添加部分测试数据,将委托指向判断正整数的方法,最后遍历输出过滤后的数组数据;
List<int> listOne = new List<int>(); listOne.Add(1); listOne.Add(-4); listOne.Add(8); listOne.Add(-6); listOne.Add(13); FilterDelegate fd = isZhengshu; List<int> listResult = Filter(listOne, isZhengshu); foreach (int i in listResult) { Console.WriteLine(i); }
运行后,显示:1 8 13
通过一个小例子,可以得出一个小结论:C# 中的委托类似于 C 或 C++ 中的函数指针。使用委托使程序员可以将方法引用封装在委托对象内。然后调用该委托对象就可以执行委托对象内方法引用指向的方法,而不必在编译时知道将调用哪个方法(如参数为委托类型的方法,也就是提供了为程序回调指定方法的机制)。”摘录自MSDN;
邹老师的通俗说法是:“就是一个能存放很多方法的指针的调用清单(但方法签名必须和委托类型签名一样),你一调用这个清单,那么清单里的所有的指针所对应的方法就会依次被执行。”
而委托的原理是神马?这里就需要跟随邹老师的博文走走了,通过VS中自带的MSIL反编译程序,将生成后的.exe拖到工具中查看委托类型声明的代码,发现其编译前就生成了一个类;它继承了System.MulticastDelegate,包含了构造方法、BeginInvoke、EndInvoke、Invoke方法。另外MulticastDelegate则继承自Delegate类。通过Reflector反编译工具,可以看出:继承关系:编译前生成的类 –> MulticastDelegate–> Delegate,而MulticastDelegate类中有3个重要的成员,其中两个继承自 Delegate:
这三者的作用分别是:
_methodPtr 里保存的就是 方法指针。
_target 里用来保存方法所在的对象。
_invocationList 其实使用时是个object数组,在注册多个方法时,其他方法就保存在此成员中,而它也就是 委托链 的关键容器。--摘自邹老师的博文;
2.事件闪亮出场
下面来看一个通过委托实现打招呼Greeting的例子(感谢张子阳先生的博文,此例选自其博文)
1.两种不同的Greeting方式:
static void ChineseGreeting(string name) { Console.WriteLine("早上好,"+name); } static void EnglishGreeting(string name) { Console.WriteLine("Morning,"+name); }
2.声明一个委托:
public delegate void GreetingDelegate(string name);
3.封装一个类,其中包含一个事件:
public class GreetingManager { public GreetingDelegate onGreeting; public void ProcessGreeting(string name) { if (onGreeting != null) { onGreeting(name); } } }
4.客户端调用:
GreetingManager gm = new GreetingManager(); gm.onGreeting += EnglishGreeting; gm.onGreeting += ChineseGreeting; gm.ProcessGreeting("Edison Chou");
5.运行显示结果:
Hello,Edison Chou
你好,Edison Chou
这里有一个问题,如果将委托声明为private权限,那么:“这简直就是在搞笑。因为声明委托的目的就是为了把它暴露在类的客户端进行方法的注册,你把它声明为private了,客户端对它根本就不可见,那它还有什么用?”-(摘自张子阳的原话)
那么如果就将委托设置为public,则客户端可以清空监听(即设置为null,因为它是引用类型),也可以伪造监听(即直接调用委托),破坏了其封装性。最后,第一个方法注册用“=”,是赋值语法,因为要进行实例化,第二个方法注册则用的是“+=”。但是,不管是赋值还是注册,都是将方法绑定到委托上,除了调用时先后顺序不同,再没有任何的分别,这样不是让人觉得很别扭么?
该怎么解决呢?于是Event事件闪亮登场了!!!它封装了委托类型的变量,使得:在类的内部,不管你声明它是public还是protected,它总是private的。在类的外部,注册“+=”和注销“-=”的访问限定符与你在声明事件时使用的访问符相同。
于是,我改写GreetingManager类:
public class GreetingManager { public event GreetingDelegate onGreeting; public void ProcessGreeting(string name) { if (onGreeting != null) { onGreeting(name); } } }
这里仅仅是加了一个event标志,没有神马大的改变。但通过Reflector反编译,可以看出事件其实就是一个封装了的私有的委托而已,还包含两个方法:add和remove;这两个方法分别用于注册委托类型的方法和取消注册。实际上也就是: “+= ”对应 add_ProcessGreeting,“-=”对应remove_ProcessGreeting。而这两个方法的访问限制取决于声明事件时的访问限制符。所以,这下客户端只能注册、注销事件,无法进行伪造和清空事件,保证了封装性。
3.委托和事件的区别
委托和事件没有可比性,因为委托是类型,事件是对象。而委托的对象(用委托方式实现的事件)与标准event方式实现的事件的区别是:事件的内部是用委托实现的。