现在的位置: 首页 > 综合 > 正文

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

2017年11月02日 ⁄ 综合 ⁄ 共 32607字 ⁄ 字号 评论关闭
分类: C#2009-03-21 12:20 165人阅读 评论(0) 收藏 举报

委托与事件对于接触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是这个方法的说明

fg 

图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();

        }

    }

}

运行程序,两种代码的写法结果一样。

 fsd

分享到: 
  • 上一篇:使用对称加密与解密算法对文件加密与解密

  • 感觉委托:就是功能外包,forward,callback 遵循相同的函数接口
  • C++模仿功能:combine function and bind :advantage:effecience ,generalization,flexiblity to parameters;
  • 对于多播功能,可以用vector<function> 模仿


  • 分类: C#2009-03-21 12:25 130人阅读 评论(0) 收藏 举报

    在C#程序设计中所谓事件,就是由某个对象发出的消息。这个消息标志着某个特定的行为发生了,或者某个特定的条件成立了。比如用户点击了鼠标,这一单击就会引起Windows给按钮消息处理程序发送一个消息,这就是OnClick事件。在深入讲解事件之前,必须先弄明白几个概念。

    发布者(publisher)与订阅者(subscriber),一个消息的发送者 (Sender)称之为发布者,而该消息的接收者 (Receiver),则称之为订阅者,订阅者需要在发布者那里注册自己,发布者在某个个条件满足的时候,通知订阅者做适当的动作或者操作。发布者与订阅者的这种模式要求二者之间的耦合性很小,而且消息要一定由发布者发出。

    针对发布者与订阅者模式,分析一下事件发生的整个过程,发布者的主要任务就是在条件合适的时候发布消息给订阅者,触发订阅者的某些动作,其实就是调用订阅者的一些方法。订阅者在事件发生前,需要将自己注册到发布者那里,以便发布者发布消息时能够通知到自己。在这个过程中需要订阅者注册自己到发布者,以及要满足发布者某个条件的一个环境。这个环境就是客户端(Client)。所以三个参与者的职责分别是:

    1)  发布者:只管发布信息给订阅者,而不管订阅者是什么。

    2)  订阅者:需要注册自己到发布者,然后实现自己受到信息后的动作

    3)  客户端:触发发布者发送信息,提供订阅者注册到发布者的环境。

    8.2.1 C#事件的模拟

    在c#中,委托是事件的实现基础,前面已经介绍过委托。现在使用委托来模拟一下事件。在模拟例子EventSimulate中,分别用类Publisher表示发布者,用类Receiver,用具有main函数的类Program模拟客户端。类Receiver中两个方法用来表示当接到发布者的信息时的动作,这两个方法分别是OnClick_1与OnClick_2。它们都有一个string类型的参数,用来表示发布消息的名字。类Publisher有一个方法SendMessage,这个方法用来表示触发发布信息的条件,即在main()函数里调用这个方法就表示事件发生了,可以发送信息。为了让订阅者能够注册到发布者,以及发布者能够调用订阅者的方法,需要有一个委托,这个委托叫ClickHandler。分析到这里,具体还是从看程序的源代码。

    类Publisher的代码

    namespace EventSimulate

    {

        class Publisher

        {

            private string name;

            public ClickHandler Click;

            /// <summary>

            /// 本方法需要在外部调用,用来触发事件

            /// </summary>

            public void SendMessage()

            {

                Click(name);

            }

            /// <summary>

            /// 构造函数,用来初始化名字

            /// </summary>

            public Publisher()

            {

                name = "Publisher";

            }

        }

    }

    类Receiver的代码

    namespace EventSimulate

    {

        class Receiver

        {

            public void OnClick_1(string name)

            {

                Console.WriteLine("我是由{0}触发以后,执行的第一个动作。",name);

            }

            public void OnClick_2(string name)

            {

                Console.WriteLine("我是由{0}触发以后,执行的第二个动作。",name);

            }

        }

    }

    客户端的源代码

    namespace EventSimulate

    {

        public delegate void ClickHandler(string name);

        class Program

        {

            static void Main(string[] args)

            {

                Publisher publisher = new Publisher();

                Receiver receiver = new Receiver();

                #region 开始订阅者注册到发布者的过程

                publisher.Click = receiver.OnClick_1;

                publisher.Click += receiver.OnClick_2;

                #endregion 结束订阅者到发布者的注册

                publisher.SendMessage();

                Console.ReadKey();

            }

        }

    }

    运行程序的结果

     r

    程序好像达到了我们的要求,好像用委托就能够实现事件了。在客户端把代码改成如下形式:

    namespace EventSimulate

    {

        public delegate void ClickHandler(string name);

        class Program

        {

            static void Main(string[] args)

            {

                Publisher publisher = new Publisher();

                Receiver receiver = new Receiver();

                #region 开始订阅者注册到发布者的过程

                publisher.Click = receiver.OnClick_1;

                publisher.Click += receiver.OnClick_2;

                #endregion 结束订阅者到发布者的注册

                //publisher.SendMessage();

                publisher.Click("Publisher");

                Console.ReadKey();

            }

        }

    }

    注意代码中把publisher.SendMessage();一行换成了publisher.Click("Publisher");,也就是说现在信息的发布者不再是publisher了,变成了客户端。分析一下原因,原来主要原因是因为委托Click在类Publisher中是公开的,为了不让信息的发布在类外进行,那么需要把委托Click变成类Publisher的私有变量。但是,委托Click变成类Publisher的私有变量后,订阅者在客户端没办法注册自己到发布者了。你可能立即想到了,类的字段可以通过字段封装使之称为外在的属性,从而在类外的操作。但是委托毕竟不同于一般的类型,如果像一般类型那样封装,仍然达不到禁止在类外发布信息的目的。这个时候该事件出场了。

    C#中的事件使用关键字event来声明,它封装了委托类型的变量,使得委托类型的变量在发布者类的内部,不管声明它是public还是protected,该变量总是发布者类的private的变量。在类的外部,订阅者注册使用符号“+=”,订阅者注销使用符号“-=”。发布者类的外部对时间的访问权限来源于在声明事件时使用的访问符。所以说,事件是特殊类型的多路广播委托,仅可从声明它们的类或结构(发布者类)中调用。如果其他类或结构订阅了该事件,则当发布者类引发该事件时,会调用其事件处理程序方法。所以改写类Publisher的代码,声明Click是一个事件。

    namespace EventSimulate

    {

        class Publisher

        {

            private string name;

            public event ClickHandler Click;

            /// <summary>

            /// 本方法需要在外部调用,用来触发事件

            /// </summary>

            public void SendMessage()

            {

                Click(name);

            }

            /// <summary>

            /// 构造函数,用来初始化名字

            /// </summary>

            public Publisher()

            {

                name = "Publisher";

            }

        }

    }

    相对应,客户端中的代码也必须修改成

    namespace EventSimulate

    {

        public delegate void ClickHandler(string name);

        class Program

        {

            static void Main(string[] args)

            {

                Publisher publisher = new Publisher();

                Receiver receiver = new Receiver();

                #region 开始订阅者注册到发布者的过程

                publisher.Click+= receiver.OnClick_1;

                publisher.Click += receiver.OnClick_2;

                #endregion 结束订阅者到发布者的注册

                publisher.SendMessage();

                //publisher.Click("Publisher");

                Console.ReadKey();

            }

        }

    }

    注意,代码中publisher.Click+= receiver.OnClick_1;这条语句原来前面使用的是publisher.Click= receiver.OnClick_1,这是委托与事件的不同之处。publisher.Click("Publisher");语句将会引发一个如图8-6的错误。

     r

    图 8-6

    把生成的代码反编译一下,观察一下事件Click的真是面目。如图8-7所示

     3

    图8-7

    事件Click的确被编译为私有字段,而且还有两个方法add_Click与remove_Click方法。具体看一下方法add_Click。

    [MethodImpl(MethodImplOptions.Synchronized)]

    public void add_Click(ClickHandler value)

    {

        this.Click = (ClickHandler) Delegate.Combine(this.Click, value);

    }

    原来是使用了Delegate.Combine方法将需要注册的订阅者类的方法加入到了该委托的委托列表中了。而remove_Click方法是使用了Delegate.Remove方法将需要注销的订阅者类的方法从委托列表删除掉了。

    [MethodImpl(MethodImplOptions.Synchronized)]

    public void remove_Click(ClickHandler value)

    {

        this.Click = (ClickHandler) Delegate.Remove(this.Click, value);

    }

    8.3 EventHandler委托

    在C#中,对于事件的处理是通过委托进行的,事件就是对发布者类中委托字段的封装,在客户端可以对发布者类提供的方法“+=”或者“-=”来注册订阅者或者注销订阅者。同时通过发布者类中公开的引发事件的方法引发事件,由发布者类发出调用订阅者方法的信息。上例中的委托ClickHandler是自己定义的。其实在系统内部已经提供了一个预先定义好的委托EventHandler。EventHandler的标准签名定义一个没有返回值的方法,其定义的格式是

    [SerializableAttribute] 
    [ComVisibleAttribute(true)] 
    public delegate void EventHandler (Object sender,EventArgs e)

    该委托中定义了两个函数参数,Object sender表示事件源,其实就是事件的发布者,而第二个参数EventArgs e是一个EventArgs类的实例。EventArgs类是包含事件数据的类的基类。其继承层次结构如图8-8所示.

    4 

    图8-8

    此类不包含事件数据,在事件引发时不向事件处理程序传递状态信息的事件会使用此类。如果事件处理程序需要状态信息,则应用程序必须从此类派生一个类来保存数据。

    下面来演示向事件处理程序传递状态信息的一个实现。例子名称是EventHandlerSample。

    因为要向事件处理程序传递信息,所以必须从EventArgs类中派生出一个新类,用来记录事件发生时的相关信息,在该示例中这个派生类为InforEventArgs。InforEventArgs类中保存了事件发生时的点的坐标,所以InforEventArgs类有两个字段x和y,分别记录事件发生时点的横坐标与纵坐标。在类内对x与y字段进行了封装,并添加了构造函数,使得InforEventArgs类的对象生成的时候初始化。

    因为委托EventHandler不能向订阅者传递信息,所以在本例中模仿EventHandler定义了新的委托PointEventHandler,该委托的定义与EventHandler基本相同,形式是:public void delegate PointEventHandler(Object sender, InforEventArgs e);。区别在于第二个函数参数e能够携带相关信息。

    信息发布者为Publisher类,Publisher类中有对PointEventHandler委托封装的Click事件,引发Click事件的SendMessage方法。

    订阅者为Receiver类,在本类中有响应事件的一个方法OnClick,因为要使用PointEventHandler,所以OnClick方法有两个函数参数,分别是Object sender与InforEventArgs e,在OnClick方法中将InforEventArgs类的实例e携带的信息打印出来。

    客户端依然使用具有main函数的Program类。信息的发布者在发布信息时,需要同时将事件发生时点的坐标传递给事件处理者。

    类InforEventArgs的代码

    namespace EventHandlerSample

    {

        class InforEventArgs:EventArgs

        {

            private double x;//表示横坐标

            private double y;//表示纵坐标

            /// <summary>

            /// 对私有字段x的封装

            /// </summary>

            public double X

            {

                get { return x; }

                set { x = value; }

            }

            /// <summary>

            /// 对私有字段y的封装

            /// </summary>

            public double Y

            {

                get { return y; }

                set { y = value; }

            }

            /// <summary>

            /// 构造函数

            /// </summary>

            /// <param name="x">点的横坐标</param>

            /// <param name="y">点的纵坐标</param>

            public InforEventArgs(double x,double y)

            {

                this.x = x;

                this.y = y;

            }

     

        }

    }

    类Publisher的代码

    namespace EventHandlerSample

    {

        class Publisher

        {

            public event PointEventHanlder Click;

            public void SendMessage(double x,double y)

            {

                InforEventArgs infor = new InforEventArgs(x, y);

                Click(this, infor);

            }

        }

    }

    类Receiver的代码

    namespace EventHandlerSample

    {

        class Receiver

        {

            public void OnClick(Object sender, InforEventArgs e)

            {

                Console.WriteLine("事件发生时的点的坐标是({0},{1})", e.X, e.Y);

            }

        }

    }

    客户端的代码

    namespace EventHandlerSample

    {

        public delegate void PointEventHanlder(Object sender, InforEventArgs e);

        class Program

        {       

            static void Main(string[] args)

            {

                Publisher publisher = new Publisher();

                Receiver receiver = new Receiver();

                #region 开始将订阅者的方法注册到发布者

                publisher.Click +=new PointEventHanlder(receiver.OnClick);

                #endregion 结束注册

                //事件的引发

                publisher.SendMessage(50.0, 100.0);

                Console.ReadKey();

            }

        }

    }

    程序的运行结果

     5

    讲到目前为止,你可能已将发现,尽管委托没有强制规定返回值的类型,但是大多数委托的返回值都是void,你可能会有疑问,为什么?

    其实因为委托变量可以供多个订阅者注册,如果定义了返回值,那么多个订阅者的方法都会向发布者返回数据,结果就是后面一个返回的方法值会将前面的返回值覆盖掉了,因此,实际上只能获得最后一个方法调用的返回值。除此以外,发布者和订阅者之间要求是松耦合的,发布者根本不关心谁订阅了它的事件、为什么要订阅,更别说订阅者的返回值了,所以返回订阅者的方法返回值大多数情况下根本没有必要。下面通过示例NoVoidDelegate来测试一下上面的说法。

    示例NoVoidDelegate使用五个类:发布者Publisher类,订阅者Receiver_1, Receiver_2, Receiver_3类,客户端Program类,与前面示例不同的地方是,本例中使用了一个委托StringDelegate,它返回一个字符串类型的数据。

    每个订阅者类中包含一个方法OnClick,它们都被注册到Publisher类的Click事件,在每个OnClick方法中,返回一个字符串,而且使用静态方法Thread.Sleep让每个方法都延迟2秒执行。

    客户端调用Publisher类的SendMessageClick方法来触发Click事件。在事件Click调用结束,打印出一句话,告诉用户SendMessageClick方法执行完毕。

    该示例完整的代码

    using System;

    using System.Collections.Generic;

    using System.Text;

    using System.Threading;

     

    namespace NoVoidDelegate

    {

        //定义返回字符串的委托

        public delegate string StingDelegate();

        class Program

        {

            static void Main(string[] args)

            {

                Publisher publisher = new Publisher();

                Receiver_1 receiver1 = new Receiver_1();

                Receiver_2 receiver2 = new Receiver_2();

                Receiver_3 receiver3 = new Receiver_3();

                #region 开始注册订阅者到发布者

                publisher.Click += new StingDelegate(receiver1.OnClick);

                publisher.Click += new StingDelegate(receiver2.OnClick);

                publisher.Click += new StingDelegate(receiver3.OnClick);

                #endregion

                publisher.SendMessage();

                Console.ReadKey();

            }

        }

        class Publisher

        {

            //定义事件Click

            public event StingDelegate Click;

            //引发事件Click的方法,并将事件的处理结果打印出来

            public void SendMessage()

            {

                Console.WriteLine("事件执行完毕的返回结果是:{0}",Click());

                Console.WriteLine("事件引发完毕");

            }

        }

        /// <summary>

        /// 订阅者1

        /// </summary>

        class Receiver_1

        {

            public string OnClick()

            {

                Thread.Sleep(TimeSpan.FromSeconds(2));

                Console.WriteLine("订阅者1的处理方法被延迟两秒!");

                return ("订阅者1的处理方法");

            }

        }

        /// <summary>

        /// 订阅者2

        /// </summary>

        class Receiver_2

        {

            public string OnClick()

            {

                Thread.Sleep(TimeSpan.FromSeconds(2));

                Console.WriteLine("订阅者2的处理方法被延迟两秒!");

                return ("订阅者2的处理方法");

            }

        }

        /// <summary>

        /// 订阅者3

        /// </summary>

        class Receiver_3

        {

            public string OnClick()

            {

                Thread.Sleep(TimeSpan.FromSeconds(2));

                Console.WriteLine("订阅者3的处理方法被延迟两秒!");

                return ("订阅者3的处理方法");

            }

        }

    }

    程序的运行结果

     6

    程序的运行结果表明:只有处于委托列表的最后一个方法的返回值被打印了出来。这个就是大部分委托返回值是void类型的原因。

    通过示例NoVoidDelegate的运行,可以看到程序不是立即就执行完毕,而是有一定的延迟,过一段时间才打印出一条信息,最后才打印出发布者执行完毕的信息“事件引发完毕”信息。这样的执行过程给我们传达了另一个信息,发布者发布消息以后,订阅者得到消息,开始执行事件处理程序。在事件处理程序执行期间,发布者将会等待。一直到所有订阅者的事件处理程序执行完毕,发布者才能继续执行。

    8.4 事件的异步调用

    示例NoVoidDelegate表明,发布者与订阅者目前的关系很紧密。但是在很多情况下,尤其是远程调用的时候(比如说在Remoting中),发布者和订阅者应该是完全的松耦合,发布者不关心谁订阅了它、不关心订阅者的方法有什么返回值、不关心订阅者会不会抛出异常,当然也不关心订阅者需要多长时间才能完成订阅的方法,它只要在事件发生的那一瞬间告知订阅者事件已经发生并将相关参数传给订阅者就可以了。然后它就应该继续执行它后面的动作,在例NoVoidDelegate中就是打印“事件引发完毕”。而订阅者不管如何执行自己的事件处理程序都不应该影响到发布者,但在上面的例子中,发布者却不得不等待订阅者的方法执行完毕才能继续运行。

    现在看一下如何能够解决这个问题?回顾一下之前提到的内容,委托的定义会生成继承自MulticastDelegate的类,新类中包含Invoke()、BeginInvoke()和EndInvoke()方法。当直接调用委托时,实际上是调用了Invoke()方法,它会中断调用它的客户端,然后在客户端线程上执行所有订阅者的方法(客户端无法继续执行后面代码),最后将控制权返回客户端。注意到BeginInvoke()、EndInvoke()方法,在.Net中,异步执行的方法通常都会配对出现,并且以Begin和End作为方法的开头。它们用于方法的异步执行,即是在调用BeginInvoke()之后,客户端从线程池中抓取一个闲置线程,然后交由这个线程去执行订阅者的方法,而客户端线程则可以继续执行下面的代码。

    BeginInvoke()接受“动态”的参数个数和类型,为什么说“动态”的呢?因为它的参数是在编译时根据委托的定义动态生成的,所有参数中前面参数的个数和类型与委托定义中接受的参数个数和类型相同,最后两个参数分别是AsyncCallback和Object类型,现在,我们仅需要对这两个参数传入null就可以了。

    另外需要注意在委托类型上调用BeginInvoke()时,此委托对象只能包含一个目标方法,所以对于多个订阅者注册的情况,必须使用GetInvocationList()获得所有委托对象,然后遍历它们,分别在其上调用BeginInvoke()方法。如果直接在委托上调用BeginInvoke(),会抛出异常,提示“委托只能包含一个目标方法”。

    以NoVoidDelegate例子为原型,对其进行修改,改造成示例EventBeginInvoke,该示例使用系统提供的预设的EventHandler作为委托,在SendMessage方法中使用了GetInvocationList()得到事件Click的委托列表,然后对委托列表中的每个对象采用BeginInvoke()方法调用。

    程序完整的源代码

    namespace EventBeginInvoke

    {

        class Program

        {

            static void Main(string[] args)

            {

                Publisher publisher = new Publisher();

                Receiver_1 receiver1 = new Receiver_1();

                Receiver_2 receiver2 = new Receiver_2();

                Receiver_3 receiver3 = new Receiver_3();

                #region 开始注册订阅者到发布者

                publisher.Click += new EventHandler(receiver1.OnClick);

                publisher.Click += new EventHandler(receiver2.OnClick);

                publisher.Click += new EventHandler(receiver3.OnClick);

                #endregion

                Console.WriteLine("main中调用SendMessage函数");

                publisher.SendMessage();

                Console.WriteLine("返回到客户端");

                Console.ReadKey();

            }

        }

        //Publisher类的定义

        class Publisher

        {

            //定义事件Click

            public event EventHandler Click;

            //发送信息,引发Click事件

            public void SendMessage()

            {

                Console.WriteLine("引发Click事件开始......");

                //得到事件Click的委托列表

                Delegate[] delArray = Click.GetInvocationList();

                //开始遍历数组delArray中的每个元素,分别进行异步调用

                foreach (Delegate item in delArray)

                {

                    EventHandler task=(EventHandler)item;

                    task.BeginInvoke(thisEventArgs.Empty, nullnull);

                }

                         

                Console.WriteLine("事件Click调用过程结束");

            }

        }

        /// <summary>

        /// 订阅者1

        /// </summary>

        class Receiver_1

        {

            public void OnClick(Object sender, EventArgs e)

            {

                Thread.Sleep(TimeSpan.FromSeconds(2));

                Console.WriteLine("订阅者1的处理方法被延迟两秒!");           

            }

        }

        /// <summary>

        /// 订阅者2

        /// </summary>

        class Receiver_2

        {

            public void OnClick(Object sender, EventArgs e)

            {

                Thread.Sleep(TimeSpan.FromSeconds(2));

                Console.WriteLine("订阅者2的处理方法被延迟两秒!");            

            }

        }

        /// <summary>

        /// 订阅者3

        /// </summary>

        class Receiver_3

        {

            public void OnClick(Object sender, EventArgs e)

            {

                Thread.Sleep(TimeSpan.FromSeconds(2));

                Console.WriteLine("订阅者3的处理方法被延迟两秒!");           

            }

        }

     

    }

    程序的运行结果。

     7

    在程序的运行中,你会看到,返回到客户端以后,即输出“返回客户端”这句话以后,后面三句话隔上一段时间才逐条输出。说明在本程序中达到了事件异步调用的目的。






  • 分类: C#2009-03-21 12:30 160人阅读 评论(0) 收藏 举报

     

    与委托相关的知识点有很多,本节仅仅就委托的协变和逆变、匿名方法、方法组转换与Lambda表达式做一些介绍。

    8.5.1 委托的协变和逆变

    在面向对象设计程序中,允许把派生类的对象赋值给基类的对象,如果从继承关系来看,允许从底层对象向高层对象的转换,并且这种转换是自动的。根据这一规律,对于委托来说可以得出两个有用的知识点。这个就是协变与逆变。

    协变:从一个方法或者函数的返回类型来看,返回子类的函数或者方法都可以绑定到具有基类返回类型的委托上面。例如:如何将委托与具有返回类型的方法一起使用,这些返回类型派生自委托签名中的返回类型。由 SecondHandler 返回的数据类型是 Dogs 类型,它是由委托中定义的 Mammals 类型派生的。

    class Mammals
    {
    }
     
    class Dogs : Mammals
    {
    }
     
    class Program
    {
        //定义一个委托,该委托返回类型是Mammals
        public delegate Mammals HandlerMethod();
     
        public static Mammals FirstHandler()
        {
            return null;
        }
     
        public static Dogs SecondHandler()
        {
            return null;
        }
     
        static void Main()
        {
            HandlerMethod handler1 = FirstHandler;
     
            //协变允许委托这样绑定方法
            HandlerMethod handler2 = SecondHandler;
        }
    }

    逆变:如果从函数或者方法的参数来考虑,那么具有基类参数的函数或者方法都可以绑定到具有子类参数签名的委托上面。比如,现在可以创建一个接收 EventArgs输入参数的事件处理程序MultiHandler,然后,可以将该处理程序与发送 MouseEventArgs 类型(作为参数)的 Button.MouseClick 事件一起使用,也可以将该处理程序与发送 KeyEventArgs 参数的 TextBox.KeyDown 事件一起使用。因为MouseEventArgs与KeyEventArgs都是从EventArgs派生出来的子类。

    System.DateTime lastActivity;
    public Form1()
    {
        InitializeComponent();
        lastActivity = new System.DateTime();
        this.textBox1.KeyDown += this.MultiHandler;    this.button1.MouseClick += this.MultiHandler; 
    }
     
    private void MultiHandler(object sender, System.EventArgs e)
    {
        lastActivity = System.DateTime.Now;
    }

    8.5.2匿名方法

    在程序中,经常有这样一些需求:

    1)需要一个临时方法,这个方法只会使用一次,或者使用的很少。

    2)这个方法的方法体很短,以至于比方法声明都短。

    在以前,即使有上面的情况,我们还是不得不在类里面写出这两个方法。增加了类代码中小型方法的混乱。而且要想使委托工作,委托也要求方法必须已经存在。可是通过匿名方法,上面的情况都可以避免。

    匿名方法可以将代码块传递为委托参数。用匿名方法定义委托的语法与前面的定义并没有什么区别。例如

    delegate int Del(int x);

    Del d = delegate(int k) { return k*k  };

    可以看到使用格式:

    delegate(参数类型  参数){ 对参数加工的语句块;}

    能够让系统生成一个方法或者函数,只是这个委托没有具体的名字,这个方法或者函数可以直接绑定给具有相同方法签名的委托。可能你已经注意到,delegate(int k) { return k*k  };中并没有显式给出返回值的类型,这个工作系统会自动的完成,可以根据代码的返回值自动推断出类型。

    匿名方法最明显的好处就是可以降低重新写一个方法的工作量,特别是这些方法的实现语句很小,或者被使用的机会很少。另外一个好处就是可以访问调用者的变量,降低传参数的复杂度,下面就通过一些使用例子来具体看看。

    假设现在有一个城市名称的列表list,现在需要从list中找到符合一定条件的城市的名称,并打印出来。比如找到list中所有以字符x与y开头的城市名称。这些要求可能很多。按照传统的写法,每一个要求都需要写成一个方法。下面就是例子AnonymityMethod的代码:

    namespace AnonymityMethod

    {   

        class Program

        {        

            static void Main(string[] args)

            {

                List<string> list = new List<string>();

                list.Add("xian");

                list.Add("shanghai");

                list.Add("beijing");

                list.Add("nanjing");

                list.Add("xuchang");

                list.Add("nanchang");

                list.Add("yanan");

                list.Add("xiangfan");

                list.Add("yuncheng");

                //查找以x开始的城市名称结果放入ResultList

                List<string> ResultList = list.FindAll(FindCitysByFirstX);

                foreach (string item in ResultList)

                {

                    Console.WriteLine(item);

                }

                //查找以y开始的城市名称,结果放入ResultList

                ResultList = list.FindAll(FindCitysByFirstY);

                foreach (string item in ResultList)

                {

                    Console.WriteLine(item);

                }

                Console.ReadKey();

            }

            //找到以x开始的城市名

            public static bool FindCitysByFirstX(string city)

            {

                return city[0] == 'x';

     

            }

            //找到以y开始的城市名

            public static bool FindCitysByFirstY(string city)

            {

                return city[0] == 'y';

            }       

        }

    }

    例子中有两种规则,那么就要定义两个方法,如果使用匿名方法的话,代码就会很简单,把例子AnonymityMethod改写成例子AnonymityMethod2。

    namespace AnonymityMethod2

    {

        class Program

        {

            static void Main(string[] args)

            {

                List<string> list = new List<string>();

                list.Add("xian");

                list.Add("shanghai");

                list.Add("beijing");

                list.Add("nanjing");

                list.Add("xuchang");

                list.Add("nanchang");

                list.Add("yanan");

                list.Add("xiangfan");

                list.Add("yuncheng");

                //查找以x开始的城市名称结果放入ResultList

                List<string> ResultList = list.FindAll(delegate(string city)

                                                         {

                                                             return city[0] == 'x';

                                                         }

                                                       );

                foreach (string item in ResultList)

                {

                    Console.WriteLine(item);

                }

                //查找以y开始的城市名称,结果放入ResultList

                ResultList = list.FindAll(delegate(string city)

                                            {

                                                return city[0] == 'y';

                                            }

                                          );

                foreach (string item in ResultList)

                {

                    Console.WriteLine(item);

                }

                Console.ReadKey();

            }

     

        }

    }

    使用工具Reflector查看生成的代码,可以看到如图8-9所示

    1

     

    图8-9

    在图8-9中可以看到,系统还是在背后偷偷的生成了两个静态的方法,名字分别为<Main>b__0与<Main>b__1,所以生成方法的代码系统自动在背后实现了。

    匿名方法的另一个优点是可以访问调用者的变量,降低传参数的复杂度。比如现在要做一个字符串中字符替换的程序,现在替换的规则有三个,将字符串中所有的字符‘A’替换为‘1’,一个规则是把字符串的所有的字符‘B’替换为字符‘4’,最后一个是把串的所有的字符‘C’替换为字符‘8’。按照传统的方法,每一个规则在类中都需要写成一个类的方法,比如把字符A替换为1的方法。

    public string ConvertATo1(string strText)

            {

                return strText.Replace("A""1");

        }

    为了替换,不得不把整个字符串作为参数进行传递,可是如果使用匿名方法,因为调用匿名方法的主调函数中就用字符串,所以在匿名方法可以自己使用,像这样的变量,匿名方法称为外部变量或者捕获变量,匿名方法可以直接使用这种变量,而不需要另外传递进来。采用匿名方法的完整代码。

    namespace AnonymityDirect

    {

        class Program

        {

            delegate string Convert();

            private static void Test2(Convert convert)

            {

                Console.WriteLine(convert());

            }

            static void Main(string[] args)

            {

                string strText = "ABCDABCDABCDA";

     

                Test2(delegate()

                {

                    return strText.Replace("A""1");

                });

                Test2(delegate()

                {

                    return strText.Replace("B""4");

                });

                Test2(delegate()

                {

                    return strText.Replace("C""8");

                });

     

                Console.ReadLine();

            }

        }

    }

    8.5.3 Lambda 表达式

    C#首席架构师Anders Hejlsberg曾经对Lambda 表达式表达过这样的观点:Lambda 表达式(拉姆达表达式)和匿名方法其实是一件事情。唯一的不同是:他们语法表现形式不同。Lambda 表达式是在语法方面的更进一步的进化。在本质上,他们是一件事情。他们的作用都是:产生方法。即:内联方法。

    Lambda表达式的书写格式如下:

    (参数列表) => 表达式或者语句块

    其中:

    参数个数:可以有多个参数,一个参数,或者无参数。

    参数类型:可以隐式或者显式定义。当指定类型时,必须用括号将类型名称和变量名括起。

    =>符号称作 lambda 运算符,可以解释为“到”。=> 运算符具有与赋值运算符 (=) 相同的优先级,并且是右结合运算符。

    表达式或者语句块:这部分就是我们平常写函数的实现部分(函数体)。

    先看一个具体的小例子,比如计算整数平方的函数pow2的传统写法是

    int pow2(int x)

    {  

       return x*x;

    }

    把它改写成Lambda表达式就可以写成(int x)=>x*x,或者写成(x)=>x*x。

    为了作为对比,把例子AnonymityMethod2改写成LambdaSample,只需要将例子AnonymityMethod2中查找以字符‘x’开始的匿名方法相对的语句从

    List<string> ResultList = list.FindAll(delegate(string city)

    {

                                       return city[0] == 'x';

                                     }

                                    );

    改写成语句

    List<string> ResultList = list.FindAll((string s)=>s[0]=='x');就能够达到想要的结果。使用Reflector查看生成的代码,可以看到如图8-10。

    2

     

    图8-10

    可以看到图8-10与图8-9几乎一样,同时可以查看<Main>b__0或者<Main>b__1也可以看到与例子AnonymityMethod2中的代码也完全一样,说明Lambda表达式的内部实现方式与匿名方法完全一样。只是Lambda表达式更加直观,代码的可读性更好。

    当然Lambda表达式也可以比较复杂,而不仅仅是一句话,比如(x, y) => { if (x < 5) { return (x + 8) * y; } else { return y; }这种写法也是允许的。

    与匿名方法一样,Lambda表达式也可以直接读取主调函数中的局部变量。

  • 抱歉!评论已关闭.