谈谈委托(delegate) 收藏 委托(delegate): 在面向对象编程中,使用的都是对象,作为对象,它们都有很多共性,比如说,可以作为参数使用。 /**//// ///方法PayFee接收字符串作为参数 /// private void PayFee(string message) { MessageBox.Show(message); }方法PayFee(),接收的参数是字符串,我们也可以重载PayFee(),让它接收的参数为int,enum,class,struct或者其他类型,如果想接收的参数类型是个方法,是否可以呢? 可以的,既然是面向对象编程,方法也是可以作为参数使用的。只是,方法比较特殊,它自己可以接收其他参数,也可以返回值。 在一个可以接收参数、返回值的方法中,接收另一个同样可以有参数、返回值的方法作为参数,可以很容易的理解,但是执行起来,容易产生混淆。 为了避免可能会有的问题,引入的概念委托(delegate)。我们常用委托,比如请人代缴手机费,好像很简单啊,说一声,帮我代缴,回来给你钱。这样做,一般情况下,都会正常实现,或许因为你委托的人同你比较熟悉,可以信赖。但让我们分析一下,这其中可能遇到的问题: 1.委托人同意替你缴费,但是由于种种原因,忘记了替你缴费,或者忘记了当月缴费,导致了手机停机; 2.委托人不但没有替你缴费,还对你说已经缴费,并收了你的钱。 以上情况都可能会发生,也许概率很低,只有0.1%。然而在程序设计中,如果使用的委托也存在这样的概率,那么我们会发现陷入了灾难当中。所以,必须要求委托是安全的,可靠的。 委托的语法要求严格,需要指明作用域,接收参数的类型及数量和返回类型。 public delegate void DoPayFee(string message); 以上声名的委托,作用域是Public的,定义接收的参数(一个方法),这个方法接收的参数是字符串string,没有返回值。 这样就可以接收上面的PayFee()作为参数使用了。 //委托实例,即要帮你缴费的中间人,他接收PayFee()这个方法,即你要缴费 DoPayFee dopayfee=new DoPayFee(PayFee) //执行代缴费 dopayfee("帮我代缴电话费"); 这样,你的要求肯定会被执行。 多点委托:(MultiCasting) 现实当中,经常出现一个人接收多个人的委托作相似的事情。比如帮多个人代缴电话费,这件事的共同点是到相同的地方,见相同的人,作相同的事情,唯一的不同是,缴费金额不同。 在程序设计中,我们称之为多点委托。 //3个委托实例,接收相同的方法作参数,也可以是不同的 //方法,但是返回类型和参数必须相同 DoPayFee payuser1=new DoPayFee(PayFee); DoPayFee payuser2=new DoPayFee(PayFee); DoPayFee payuser3=new DoPayFee(PayFee); //实现多点委托 DoPayFee paytotal=payuser1+payuser2+payuser3; //执行 paytotal("给我们缴费"); 因为最后的参数是由多点委托统一接收的,即每个单独的委托都接收相同的参数,所以我们只能给它口信,让它帮我们代缴费,至于金额,需要定义不同的PayFee方法,PayFee1(),PayFee2(),PayFee3()来完成。 在很多的书籍中提到,使用多点委托,其调用的方法必须返回void,即不能够返回值,如上面的例子。但是并不是不可以在多点委托中使用带有返回值的方法,我们可以定义: //3个返回值得方法,假设返回值代表缴费金额 private int PayFee1(string message) { } private int PayFee2(string message) { } private int PayFee3(string message) { }定义了上面的3个方法,让缴完费后,告诉我们费用是多少。 然后是用多点委托进行缴费: //委托实例 DoPayFee payuser1=new DoPayFee(PayFee1); DoPayFee payuser2=new DoPayFee(PayFee2); DoPayFee payuser3=new DoPayFee(PayFee3); //多点委托 DoPayFee paytotal=payuser1+payuser2+payuser3; //执行,count代表缴费金额 int count=paytotal("给我们缴费"); 执行完paytotal,只得到了一个结果,它覆盖了前面委托执行后的结果,即最后执行的委托调用的方法的结果,这同我们的初衷,得到每个缴费金额不同,有什么方法解决呢? //循环执行每个委托,获取结果 foreach(DoPayFee payfee in paytotal.GetInvocationList) { int count=payfee("缴费"); } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 委托的理解和使用 “委托”,delegate,其实我觉得翻译成“代表”还好一点,它“代表”一个样子跟他差不多的方法。例如有以下方法: private int DoSomething(string s) {...} 和以下委托(还是入乡随俗吧,我也叫委托好了): public delegate int DoThingsDelegate(string s); 那么,因为样子差不多(术语叫“特征”(signature)相同——参数列表和返回值一样),这个委托就可以“代表”上面的方法。具体的“代表”的方法是: private DoThingsDelegate varDoThingsDelegate; // 定义委托变量 varDoThingsDelegate = new DoThingsDelegate(DoSomething) // 将方法与委托连接起来 int returnValue = varDoThingsDelegate("hello!"); // 使用委托调用被代表的方法 以上三步缺一不可,但是第二步通常都不是我们自己做的——如果我们自己已经知道了方法名字,为什么不直接调用方法呢?我们通常都是1)定义委托,2)定义委托变量,3)让别人对委托变量赋值,4)把委托变量看成是个方法来调用。 这就推断出委托的使用场合:当我们要调用某个方法,但是我们只知道这个方法该长什么样子而不知道具体从哪里获得的时候,就是使用委托的时候。 .NET 里,有两个地方最常使用委托,首先就是事件,不过这个等下再说,然后就是异步调用方法。异步调用方法,简而言之,就是叫一个对象“开始”一项工作,但不需 要立刻返回结果,我也不等待结果,继续我自己的工作,到这个对象“完成”,可以返回结果的时候再“告诉我”。整个故事的难点就是“告诉我”——这个对象怎 么才能知道该怎么告诉我?为此,我在叫它“开始”工作的时候,顺便把我的一个方法包装成委托,告诉它“做完了之后,用这个方法告诉我”。这样,工作对象就 不需要知道我是谁/什么,完成工作之后尽管调用委托就行了。 事件摆在异步之后说,是因为事件是一种特殊的委托——可以“代表”多个同特征方法的委托。考虑以下场景:雇员wang有一个PlayGame事 件,雇员li交给wang一个委托,说“你玩游戏的时候告诉我”,然后雇员zhang也交给wang另一个委托:“你玩游戏的时候也告诉我”。那么,雇员 wang必须找个方法将所有玩游戏委托都记住,以便通知所有人,以免三缺一。这种“多播委托”(MulticastDelegate)是事件的基本模型, 在事件发生的时候由事件触发者通过调用委托来运行未知位置未知名字但已知特征的多个方法。 使用事件跟使用委托有一些区别。首先,符合委托特征的方法有了新名字:事件处理器(event handler),因为它的作用就是处理一个事件。然后,将委托交给事件所有者这个动作叫做“订阅”(subscribe),也就是说事件像周刊一样,发 行的时候你就能收到一份。使用事件的步骤是 //1, 定义委托。该委托必须无返回(void), // 因为事件如果存在多个订阅者,有返回值就乱套了。 // 委托类型的名字习惯用EventHandler或Handler结束, // 因为这个委托类型代表的是事件处理器。 public delegate void PlayGameEventHandler(Employee starter, string gameName); // public delegate void 委托类型名(参数列表); //2,在对象里定义事件,即“委托变量”——多播委托变量 public event PlayGameEventHandler PlayGame; // public event 委托类型名 事件名; //3,由其他对象操作,订阅事件 wang.PlayGame += new PlayGameEventHandler(this.wang_PlayGame); // 取消订阅只需要将上面的 += 改成 -= // 这个是事件处理器的范例: private void wang_PlayGame(Employee starter, string gameName) { Console.Write("我也要玩" + gameName + "!!"); } //4,事件发生时,调用所有订阅者的事件处理器: if (this.PlayGame != null) // 没有人订阅就会 == null this.PlayGame(this, "Tic Tac Toe"); ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 《Effective C#》Item 21:用委托实现回调 委托属于C#中的新名词,它的应用也非常广泛,例如事件就是委托最简单而又直接的例子。 那么首先说说什么是委托,其实委托在用过C或者C++的人看来就是函数指针,不过使用C#的大多数人都没有用过这两门语言,因此对委托的理解不 是很深,对于委托可以简单的从字面去理解,即“委托别人去执行某些操作”,也就是说执行一个操作,而这个操作过程自身并不知道,只是委托过来让你去执行而 已。 参看如下这个例子。 public delegate void HelloHandler( string Msg ); private void SayHello( string sMsg ) { MessageBox.Show( sMsg ); } // delegate sample HelloHandler hHello = new HelloHandler( SayHello ); hHello( "Hello World!" ); 例子很明显,在执行hHello的时候,并不知道具体的过程是什么样的,只是去执行而已。 现在要说说委托能做些什么,那么先来分析一下事件,这也是委托用得最多的地方。一般事件的初始化需要绑定一个事件处理函数,那么当事件触发的时 候,处理函数会被调用,也就是说在事件处理函数可以明确的知道,此事件是被触发了。例如,点击窗体的上的Button,它会把Click事件反馈给窗体。 很明显,委托是处理对象之间的信息交互。除了事件外,委托的大多用处都和此类似,那么这样使用的好处在于降低对象之间耦合性。 其次委托不同于以前函数指针的地方在于,可以绑定多个委托函数,例如: public delegate void HelloHandler( string Msg ); private void WriteHello1( string sMsg ) { Debug.WriteLine( "WriteHello1:" + sMsg ); } private void WriteHello2( string sMsg ) { Debug.WriteLine( "WriteHello2:" + sMsg ); } // delegate sample HelloHandler hHello = new HelloHandler( WriteHello1 ); hHello += new HelloHandler( WriteHello2 ); hHello( "Hello World!" ); 不过在这种情况下,由于绑定到委托上的函数是按照顺序执行的,所以有两个潜在的问题。 第一个问题,当一个函数执行过程中产生异常,导致后面的不能进行执行。就上面的例子而言,如果在“WriteHello1”函数中出现异常,会导致“WriteHello2”不能被执行。 另一个问题,就是委托执行的返回值,当绑定多个函数,委托执行的返回值是最后一个绑定函数执行后的返回值,那么通过这个值去做判断将会是不正确的。 那么对于委托绑定多个函数,要注意的是不要把异常扩散出来,其次委托类型的返回值为“void”。 如何去使用委托,很多人看了书上的例子,也知道委托的意义,但是无法把它和实际应用进行结合。那么在使用委托的时候,首先要明白几个问题,等各个问题清楚了,委托原型也就自然出来了。 需要分析清楚的问题有如下几点。 问题一,传递信息是什么,传递的时机是否固定,一次传递还是多次传递; 问题二,用委托是否合适; 问题三,哪一方是委托的调用端,而哪一方是委托的初始端; 问题四,委托的初始化放在哪里比较合适。 问题一是关键,这首先决定是否要使用委托来实现,其次如果要使用委托,那么需要确定委托函数类型。 对于问题二来说,很多人可能就不解了。没错,委托是可以降低类型之间的耦合性,但是能起到这种作用的并不是只有委托这一种方法。很多情况下,用 重载构造函数即可以达到这一目的,因此想问题的时候,不能局限于此。例如,很多人都做过弹出一个单独窗体对某一条记录进行修改,这里用委托可以实现,但是 考虑到DataRow属于引用类型,而这个窗体脱离了数据记录就失去了意义,因此可以重载构造函数,在初始化窗体的时候,把记录传递给窗体就行了。相对而 言,后者会更简单直接些。 有了前两个问题的分析,不少人在写委托的时候,会把顺序写反了,因此程序执行的效果并不是设想中的那样,这一点要尤为注意。 对于第四个问题来说,可以借鉴窗体的控件事件初始化部分代码,即委托的初始化采取就近原则,不过这不是唯一初始化的地方,这样写只是便于防止漏写。 好了,对于委托大致说这么多,希望对大家有所帮助

评论