l 认识委托
如果说“类函数”是减轻程序员编写的代码量,根据不同参数输入、经过统一过程计算的“公式化”模式,那么“委托”远远不仅如此——它不仅具备“类函数”的特征,同时还允许程序设计者在同参、同返回值的情况下“动态”改变函数体内容,并且可以实现“跨类调用”,所以“委托”无愧于是真正的“动态函数体”。这个特性在微软的Framework框架下有着广泛的应用(诸如“事件驱动机制”、Lambda运算式、表达式树、Linq等)。今天我们将初步了解委托的语法定义,向大家介绍“纯委托”、“匿名委托”、“多路委托”,以及利用委托设计“观察者模式”的简化应用。
l 纯委托
在C#中,委托的定义必须是这样子的:
[修饰符] delegate [返回类型] 委托名(参数列表……);
一个简单的纯委托可以这样挂接和使用:
委托名 委托实例=new 委托名(被委托函数),注意“挂接”时不能有参数列表!
委托实例([参数列表]);
下面给出一个具体的例子:
【例1】设计一个控制台应用程序,当输入端输入1,执行两个数的加法;输入2的时候执行两个数的减法(不考虑异常和其它输入等情况)。
【分析】本题的计算过程是受用户控制的(即函数过程体是动态可以变化的),但是输入、输出参数类型不发生变化。因此可以使用委托,进行动态方法调用:
显然,上面程序根据用户输入的数值“动态”调用不同的过程、却具有相同函数参数列表,以及返回值的“类函数”,实现了“动态改变函数体”的目的。
或许有些读者要发问了——难道我就不能直接调用两个函数嘛?干嘛那么复杂?——是的,这道题是为了初步解释纯委托的简单应用,打比方而已。在真正的开发中,我们往往要在不改变原有的代码情况下,动态结合我们自身增加的过程。下面就来看一个稍微复杂点的示例:
【例2】假设有一个List<int>数组(有1~10个数,这儿是泛型的用法,您只要先知道在这里的List中只能存放int以及可以转换成该类型的数据类型)。我们要设法求出偶数的数字,并输出打印。
【分析】这个问题似乎也可以使用for循环来解决,不过微软为List提供了这样一个方法:
FindAll(public delegate bool Predicate<T>(T obj))
如果您不了解泛型,那么您就可以把这个函数近似理解成如下对于int的委托参数:
FindAll(public delegate bool Predicate (int obj))
似乎微软已经为你完成了FindAll的函数部分过程(返回值是List<int>),但是判定过程还是你来写。显然,如果你要使用这个方法,你就不能简单地调用自定义的函数进行直接寻找输出了。而是必须按照它的规定来——首先看,这个委托要求输入参数是一个int,输出是一个bool,结合题目要求(求偶数),您应该会直观想到偶数的判定式“num%2==
{
return num%2==0;
}
然后开始调用,传入该函数(如下):
foreach(int i in XXXXList.FindAll(new XXXXClass().IsEvenNum))
{
Console.WriteLine(i);
}
注意:“XXXXClass”是这些方法所在的类名,“XXXXList”是您定义的某个存放int的泛型List。
通过这个例子,您应该明白了解了“动态函数体”(委托)的厉害之处——它可以在不改变原来代码的基础上,对于“同参同返回值”的过程实现自定义,而且可以和微软内部的FindAll方法进行“无缝对接”(纯粹返回一个bool不足以实现偶数集合输出,微软在FindAll内部只是简单地调用了该委托,利用外部方法实现“自定义”的函数体功能,代码参考如下):
l 匿名委托
事情总是呈现多面性的——函数除了可以被同参、同返回值的委托“挂接”外自然可以根据其特有的访问修饰符被其它类、函数调用,我们自然会认为——如果一个函数多次、频繁被外部类(或内部)调用,则这个函数的确有其存在的意义(因为减缓了重复、机械式编码疲劳)。现在的问题在于,如果一个函数只是为了“委托”的需要而刻意编写(比如仅仅为了像上面的例子一样求偶数),是不是有点不值得?不情愿?
但是“函数”又不得不写——怎么办?“留之无用,弃之可惜”这种“鸡肋”事件我们希望只是发生在曹操身上,悲剧不希望在我们这里重演。所以微软对于“只被特定委托调用”的函数设计一种特有的定义方式——匿名委托(匿名委托函数)。
匿名委托的定义:
委托名 委托示例=new 委托名
(
delegate (参数列表)
{
过程……
[返回值]
}
)
上面的纯委托就可以做如下改变:
XXXXList.FindAll(delegate (int num){return num%2==0;});
l 多路委托
除了“纯委托”、“匿名委托”之外,我们还可以将多个已经“挂接”到不同类的函数(必须都复合委托的“同参列表”、“同返回值”),然后统一用一个委托逐一遍历调用,这种方式被成为“多路委托”(或者“多播委托”)。“多路委托”的语法定义如下:
委托名 委托实例=new 委托名(被委托函数1)
委托实例+= new 委托名(被委托函数2)
委托实例+= new 委托名(被委托函数3)
………………
要移除某个委托,只需使用-=替换+=即可。
【例】使用“多路委托”实现计算输出数字30的Sin、Cos和Tan结果
【分析】Sin、Cos和Tan都具备输入参数“double”,输出参数“double”共同特性,因此可以使用一个统一委托进行挂接。代码如下:
实际上,C#的delegate是一个抽象类,其中包含了存放具备相同参数列表和返回值的委托数组,并且重写了+=和-=方法,从而使得您可以使用这些符号动态方便地添加、移除这些函数。要获取多个委托的函数进行遍历,您可以使用GetInvocationList()方法。
l 借助“委托”设计观察者模式——模拟Windows的音量调节器
“观察者模式”是经典设计模式中的一种,也是应用比较广泛的一种优秀思想。其主要实现“事件驱动”机制,同时实现不同类(对象)之间的同步问题。这儿我们以微软的“音量调节器”举一个例子——在您进入Windows桌面之后是不是注意到右下角有一个小喇叭?双击该喇叭若干次,会产生相同的窗口,不过无论你改变哪个窗口的滑动条,其余的一定随之发生变化——这就是“联动效应”,观察者模式的典型运用。
不过要实现这个功能不是很简单的,因为最麻烦的问题在于初始化了若干窗口,这些窗口的进度条如何做到完全同步(每个窗体的实例是独有的,不是共享)?
唯一的解决方案就是另外设计一个类,专门用于监听是否有窗体产生(有的话加入其监听队列),并且具备一个同步方法(将所有的成员进行同步处理)。
现在我们假设用VS设计器生成MusicForm,有一个Button,点击将生成与自己一模一样的窗体,有一个拉杆,拖拉将使得其它窗体和自身的拉杆值保持同步。要实现这样一个功能,其结构可以使用UML示例,大致图如下:
具体NotifyListener类代码如下:
Form1(MusicForm)设计代码如下:
工作原理:在NotifyListener类中声明一个存放多个Form1的泛型集合List,这样以便通过在NotifyAll方法中统一调节各个窗体的音量大小。声明NotifyEvent委托的目的在于要使在NotifyAll中统一调用各个窗体的调节音量方法(在Form1设计中有必要的实现,如ChangeVolumn)。这样,当一个窗体启动后,首次触发Load事件,将把自身加载到NotifyListener类中,当一个Button点击以后,将初始化生成一个新窗体;在拖拽拉杆(trackbar)的时候,调用NotifyAll,由于都实现了“委托”的具体方法,因此可以集中地被NotifyAll方法遍历所调用。