您的位置:首页 > 其它

委托、事件和Lmbdas表达式1----委托

2009-03-21 12:20 309 查看
委托与事件对于接触C#不长的人来说,是一个难于理解的概念,难于理解的原因我认为主要是这两个名称的影响,另外在平时编程中很少见到它们两个的使用,即使有些人使用了委托或者事件,可能也是照猫画虎,没有彻底的了解它们的本质,如果对它们的本质了解了,你会发现它们其实很简单,而且在很多地方都可以使用。Lambda表达式是c#语言新添加的特性,其实这种表达式在其他语言中很早已经出现了,Lambda表达式最早出现在LISP语言中,后来在一种叫Python的脚本语言中被灵活运用,因为Lambda表达式灵活以及一些特有的性质,c#才把它引入进来,当然引入Lambda表达式,人们对它的褒贬不一,有人认为灵活并且使用方便,但是有些人认为Lambda表达式纯粹是函数的延续或者变种,是一种编程思想的倒退。
为了彻底的了解本章的概念以及一些技术,可能需要借助一些特定的工具,其中Reflector工具可能在本章中需要经常使用。Reflector工具可以将dll或者exe文件反编译为相应语言的功能,这样就可以看到一些技术的内幕或者本质。Reflector工具是由微软员工Lutz Roeder编写的免费程序,你可以到http://www.red-gate.com/products/reflector/官方网站上下载最新的版本。
本章将由浅入深地讲述什么是委托、如何定义委托,委托的种类、为什么要使用委托、事件的由来等问题。
8.1委托
委托在汉语中,是一个动词,这个原因使很多程序员理解起来比较吃力,那么需要你首先更改的第一个观念就是委托在c#纯粹是一个名词。在c#中委托使用Delegate来表示,在c#中纯粹是一个类。一个比较特殊的类。
8.1.1 .NET委托类型的基本概念
首先来看一些委托在MSDN中的定义。委托是一种安全地封装方法的类型,它与 C 和 C++ 中的函数指针类似。与 C 中的函数指针不同,委托是面向对象的、类型安全的和保险的。委托的类型由委托的名称定义。下面再看一下委托在c#中的继承关系



委托本身的定义



c#的委托其实是一个特殊的类,这个类是一个抽象的类,它从接口ICloneable, ISerializable继承而来
通过上面的定义,应该从两方面来理解委托
第一:从委托本身或者出身来看,它只是c#中非常普通的类而已,与一般的类并没有什么不同,所以使用的方法跟一般类的使用方法相同。这一点是由委托本身的继承关系以及它本身的定义就可以得到。
第二:从委托的功能来看,委托的功能是它又定义了一个新的类型,这个新的类型就像C#中int,string类型一样,只是string类型用来表示字符串,也就是使用string类型定义的变量可以存储或者表示一个字符串,而使用委托定义的新类型是一个方法或者函数的表示,这个新类的类型的名称不固定,需要使用委托来指定。使用委托一旦定义了一个新的类型,比如类型名叫MethodVar,那么这个MethodVar类型就可以表示一个方法或者函数。
8.1.2在C#中定义委托
上面的标题严格的说起来,不能很确切的表达委托的真正含义,应该叫做在c#中使用委托来定义新类型。不过,大多数人都这么说,所以本书也采用普遍的叫法,不过,千万不要让不确切的叫法把你干扰了。
在c#中使用委托来定义新类型的语法很简单,其格式是
权限访问符 delegate 返回值类型 类型名 (参数列表);
比如public delegate string SomeDelegate(string someStringValue);就定义了新的委托,委托的名字是SomeDelegate,SomeDelegate就是一种新的类型了,这种类型与string类型的地位是一样的,只是SomeDelegate类型可以表示一个方法或者函数。SomeDelegate类型是不是能够表示所有类型的方法或者函数呢?回答是否定的。那么它能表示什么类型的函数或者方法呢?看一下这种类型的定义部分。前面的public表示了这种类型在定义位置能够被别人访问的权限,delegate是定义委托必须使用的关键字,后面的部分就说明了SomeDelegate类型所能表示的函数或者方法的类型,这里SomeDelegate类型表示的方法或者函数类型只能是有一个string形式参数并且返回值是string的函数或者方法。也就是说委托的签名(由返回类型和参数组成)应该与所表示的函数或者方法的签名相同。那么这种新的类型现在有什么用呢?具体从一个例子HelloWorld开始。
namespace HelloWorld
{
class Program
{
/// <summary>
/// 问候函数的调用函数
/// </summary>
public static void PrintHello()
{
EnglishHelloWorld();
}
/// <summary>
/// 打印出英文的Hello,World!
/// </summary>
public static void EnglishHelloWorld()
{
Console.WriteLine("Hello,World!");
}
static void Main(string[] args)
{
//程序一旦启动,首先打印出问候语,表示友好
PrintHello();
//下面开始做一些具体的工作....
Console.ReadKey();
}
}
}
上面的程序代码很简单,只是输出一个英文版本的问候语,可是,现在有一个问题出现了,假设这个软件要发布到中国。那么中国的很多用户可能不会英语,所以,软件应该打印出“世界,你好!”。于是又添加了一个打印中文问候信息的函数ChineseHelloWorld(),为了区分中文或者英文的状态,再定义一个枚举变量Language,这样函数PrintHello就可以根据语言的类型来调用不同的问候函数了,修改后的代码。
/// <summary>
/// 枚举变量,用来表示语言的种类
/// </summary>
public enum Language { Chinese, English };
/// <summary>
/// 问候函数的调用函数
/// </summary>
public static void PrintHello(Language l)
{
switch (l)
{
case Language.Chinese:
ChineseHelloWorld();
break;
case Language.English:
EnglishHelloWorld();
break;
}
}
/// <summary>
/// 打印出中文的世界,你好!
/// </summary>
public static void ChineseHelloWorld()
{
Console.WriteLine("世界,你好!");
}
尽管这样解决了问题,但我不说大家也很容易想到,这个解决方案的可扩展性很差,如果日后需要再添加韩文版、日文版,就不得不反复修改枚举和PrintHello()方法,以适应新的需求。
假如PrintHello()方法可以接受一个参数变量,这个变量可以代表另一个方法,当给这个变量赋值EnglishHelloWorld的时候,它代表着EnglishHelloWorld() 这个方法;当给它赋值ChineseHelloWorld的时候,它又代表着ChineseHelloWorld()方法。将这个参数变量命名为 MakeHelloWorld,那么在调用PrintHello()方法的时候,给这个MakeHelloWorld 参数也赋上值(ChineseHelloWorld或者EnglishHelloWorld等)。由于MakeGreeting代表着一个方法,它的使用方式应该和它被赋的方法(比如ChineseHelloWorld)是一样的。比如:MakeHelloWorld();
有了思路了,现在就来改改PrintHello()方法,那么它应该是这个样子了:PrintHello(MakeHelloWorld),MakeHelloWorld现在应该是一个表示方法或者函数的变量了。在C#中,委托就能够实现让一个变量代表一个函数或者方法的功能。
首先定义一个能够代表没有返回值与函数参数的类型
public delegate void HelloWorldDelegate();语句就定义了一个能代表ChineseHelloWorld()与EnglishHelloWorld()函数的新类型HelloWorldDelegate,这个新类型就是委托,一般在定义委托的时候,为了跟其他类型区别,需要在后面加上Delegate。有了新的类型HelloWorldDelegate就可以在PrintHello()函数的参数中传递这种类型的变量了。修改后的代码。
namespace HelloWorldDelegate
{
class Program
{
public delegate void HelloWorldDelegate();
/// <summary>
/// 问候函数的调用函数
/// </summary>
public static void PrintHello(HelloWorldDelegate MakeHelloWorld)
{
MakeHelloWorld();
}
/// <summary>
/// 打印出中文的世界,你好!
/// </summary>
public static void ChineseHelloWorld()
{
Console.WriteLine("世界,你好!");
}
/// <summary>
/// 打印出英文的Hello,World!
/// </summary>
public static void EnglishHelloWorld()
{
Console.WriteLine("Hello,World!");
}
static void Main(string[] args)
{
//程序一旦启动,首先打印出问候语,表示友好
PrintHello(ChineseHelloWorld);
//下面开始做一些具体的工作....
Console.ReadKey();
}
}
}
既然委托HelloWorldDelegate和类型string的地位一样,都是表示一种类型,那么,是不是也可以这么使用委托?下面对PrintHello()进行修改,在函数体内直接生成一个委托HelloWorldDelegate的MakeHelloWorld,对MakeHelloWorld直接赋值,看看能不能运行?PrintHello()修改后的代码
public static void PrintHello()
{
HelloWorldDelegate MakeHelloWorld;
MakeHelloWorld = ChineseHelloWorld;
MakeHelloWorld();

}
运行结果



因为委托HelloWorldDelegate本身是一个类,那么代码HelloWorldDelegate MakeHelloWorld; MakeHelloWorld = ChineseHelloWorld;在c#中到底是如何执行的?使用工具Reflector查看一下,如图8-1所示。


图8-1
看到MakeHelloWorld的生成跟类的实例化一样。并且委托本身就是类,所以以后称生成委托变量用更加正规的术语:生成委托对象。现在就有两种生成委托对象的方法,直接赋值与使用new关键字。
例子HelloWorld中对委托绑定的函数或者方法都是静态方法,这样的绑定称为静态绑定,委托还可以绑定动态方法或者函数,相应的称为动态绑定。绑定方法或者函数时,要注意下面的规则:
1) 如果与静态方法或者函数绑定,应该使用类名.方法名(参数)的格式。如果省略类名,c#会使用委托初始时所在的类名。图8-1中的代码显式的给出了类名Program。
2) 如果与动态的方法或者函数绑定,应该使用对象名.方法名(参数)的格式,如果省略对象名,系统会使用当前的this实例。
下面看具体的实例BindByStaticOrNo,在例子中主要演示上面的两条规则,在类Program与类ExternClass中,各自定义了一个静态方法与动态方法,为了演示忽略在静态方法前面类名的情形。这两个类的静态方法名字一样(StaticExMethod)。每个方法都有一string类型的参数mString,mString用来表示该方法绑定到委托TestStaticOrNo的字符串,在方法内部把mString输出。既然委托是一个类,那么可以在任何定义类的地方定义委托,本例中把委托TestStaticOrNo定义在一个单独的类文件TestStaticOrNo.cs中。本例的类图如图8-2。



图8-2
委托TestStaticOrNo的定义
namespace BindByStaticOrNo
{
public delegate void TestStaticOrNo(string mString);
}
类ExtenClass的定义
namespace BindByStaticOrNo
{
class ExternClass
{
/// <summary>
/// 外部类的静态方法
/// </summary>
/// <param name="mString">绑定该方法的字符串</param>
public static void StaticExMethod(string mString)
{
Console.WriteLine("我是ExternClass类的静态方法...通过{0}调用", mString);
}
/// <summary>
/// 外部类的动态方法
/// </summary>
/// <param name="mString">绑定该方法的字符串</param>
public void DynamicExMethod(string mString)
{
Console.WriteLine("我是ExternClass类的动态方法...通过{0}调用", mString);
}

}
}
类Program的定义
namespace BindByStaticOrNo
{
class Program
{

static void Main(string[] args)
{
TestStaticOrNo test = new TestStaticOrNo(Program.StaticExMethod);
//静态绑定的3种情况
test("Program.StaticExMethod");
test = new TestStaticOrNo(ExternClass.StaticExMethod);
test("ExternClass.StaticExMethod");
//忽略了类名的情形
test = new TestStaticOrNo(StaticExMethod);
test("StaticExMethod");
//动态绑定的2种情况
//先生成一个ExternClass类的实例
ExternClass externclass=new ExternClass();
Program program = new Program();
test = new TestStaticOrNo(externclass.DynamicExMethod);
test("externclass.DynamicExMethod");
test = new TestStaticOrNo(program.DynamicProgramMethod);
test("program.DynamicProgramMethod");
Console.ReadKey();
}
/// <summary>
/// Program类的静态方法
/// </summary>
/// <param name="mString">绑定该方法的字符串</param>
public static void StaticExMethod(string mString)
{
Console.WriteLine("我是Program类的静态方法...通过{0}调用", mString);
}
/// <summary>
/// Program类的动态方法
/// </summary>
/// <param name="mString">绑定该方法的字符串</param>
public void DynamicProgramMethod(string mString)
{
Console.WriteLine("我是Program类的动态方法...通过{0}调用", mString);
}
}
}
程序的运行结果



运行程序的第三行比较重要,验证了上面的规则。
8.1.3委托的类型
在上一小节的实例中,委托只绑定了一个方法,这样的委托称为“单播委托”,委托也可以同时绑定多个方法,这样的委托称为“多播委托”,多播委托拥有一个带有链接的委托列表,该列表称为调用列表,它包含一个或多个元素。在调用多路广播委托时,将按照调用列表中的委托出现的顺序来同步调用这些委托。多播委托属于MulticastDelegate类。MulticastDelegate是Delegate的子类。图8-3显示了MulticastDelegate类的继承关系。



图8-3
其定义是



MulticastDelegate是一个特殊类。编译器和其他工具可以从此类派生,但是您不能显式地从此类进行派生。Delegate类也是如此。多播的使用跟单播的方法基本一样。如果定义了一个委托假设为ADelegate,生成ADelegate的实例adeleggate,在生成实例的同时已经给该实例绑定了一个方法,这时实例adeleggate是一个单播委托。如果继续给该实例绑定方法,可以使用+=”符号来添加新的方法,新加入的方法将会与前面的方法存储在该实例的委托列表中,在列表中的顺序就是添加方法的顺序。同样的道理可以使用符号“-=”将某个方法从委托列表中删除。如果某个多播委托一旦被调用,那么与该委托所绑定的所有方法就会按照委托列表中的顺序依次调用。
依然使用例子HelloWorldDelegate中的代码,对方法PrintHello()进行修改,修改后的代码
public static void PrintHello()
{
HelloWorldDelegate MakeHelloWorld=new HelloWorldDelegate(ChineseHelloWorld);
//添加方法EnglishHelloWorld,使MakeHelloWorld称为一个多播
MakeHelloWorld += EnglishHelloWorld;
//对MakeHelloWorld调用
MakeHelloWorld();
//从MakeHelloWorld去掉ChineseHelloWorld方法的绑定
MakeHelloWorld -= ChineseHelloWorld;
MakeHelloWorld();

}
运行的结果



运行Reflector工具软件,看一下PrintHello()方法反编译的结果如图8-4所示。



图8-4
在图8-4中发现反编译的代码使用了一个方法Combine来加入新的委托,而使用方法Remove来删除掉多播委托中的一个委托。于是引出生成一个多播的另外两种办法。
使用例子MulticastSample来模拟一下现实中一个人起床后的各种动作。一个人早上起床后的动作有穿衣服,洗脸,吃早餐,刷牙等。现在定义一个类Person,里面包含Wear、Eat、Wash等方法。在表示一天的OneDay类中实现一个委托MorningDelegate,用来模拟早上起来后的一系列动作。其中Person类的代码是
namespace MulticastSample
{
public class Person
{
private string name;

public static void Wear()
{
Console.WriteLine("我在穿衣服!!");
}

public static void Eat()
{
Console.WriteLine("我在吃饭!!");
}

public static void Wash()
{
Console.WriteLine("我在洗脸!!");
}
}
}
OneDay类的代码
namespace MulticastSample
{
public class OneDay
{
public delegate void MorningDelegate();
public void MoringAction()
{
MorningDelegate moring1 = new MorningDelegate(Person.Wear);
MorningDelegate moring2 = new MorningDelegate(Person.Wash);
MorningDelegate moring3 = new MorningDelegate(Person.Eat);
MorningDelegate moring = moring1 + moring2 + moring3;
moring();
}
}
}
在OneDay类中看到了生成多播的一种方法,就是一系列单播委托使用“+”连接起来。另外一种方法是使用Delegate的静态方法Combine,图8-5是这个方法的说明



图8-5
这个静态的方法可以将一个委托数组连接在一起返回一个新的委托,所以可以更改OneDay类的MoringAction()方法为另一种形式。
namespace MulticastSample
{
public class OneDay
{
public delegate void MorningDelegate();
public void MoringAction()
{
MorningDelegate []moring=new MorningDelegate[3];
moring[0] = new MorningDelegate(Person.Wear);
moring[1] = new MorningDelegate(Person.Wash);
moring[2] = new MorningDelegate(Person.Eat);
MorningDelegate multi_moring = (MorningDelegate)Delegate.Combine(moring);
multi_moring();
}
}
}
Main函数代码
namespace MulticastSample
{
class Program
{

static void Main(string[] args)
{
OneDay today = new OneDay();
today.MoringAction();
Console.ReadKey();
}
}
}
运行程序,两种代码的写法结果一样。

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: