.NET学习笔记(八) ——委托(上)

过年后就一直没写学习笔记了,书第一边已经看完了,后面从事件开始的章节有些复杂,牵扯的知识也很 多。而且最近工作也很忙,所以没有时间来写。这段时间感觉自己对委托又有了一定的认识,所以打算来聊聊 .net里的委托。

 

一 什么是委托

 

MSDN中给出的定义:

委托是用来处理其他语言(如 C++、Pascal 和 Modula)需用函数指针来处理的情况的。不过与 C++ 函数 指针不同,委托是完全面对对象的;另外,C++ 指针仅指向成员函数,而委托同时封装了对象实例和方法。

简单的说委托就是对函数指针的一个封装。所谓的函数指针,了解C++的人都应该知道,函数指针中存放的 是函数的地址,当调用是,使用这个指针就可以调用到函数。而委托只不过是给这个指针穿的件漂亮的衣服, 使他变的更安全更强大。

delegate 声明定义一种引用类型,该类型可用于将方法用特定的签名封装。委托实例 封装静态方法或实例方法。委托大致类似于 C++ 中的函数指针;但是,委托是类型安全和可靠的。

delegate  void  DeletgateEx (int x);  //声名一个委托

DeletgateEx dx =new dx(print);   //实例化一个委托并绑定一个方法  

void print (int x)
{
  Console.WriteLine(x.ToString());
}

delegate 是声明委托的关键字

  1. 定义一个委托类型,这看起来有点奇怪,和一般的定义不一样?好象是在声明一个方法。要注意,委托 是对函数指针的封装,所以你就把委托看成一个存放函数的对象。但是委托不是任何函数都可以存放,他只能 存放和他声明的函数拥有相同签名的函数。从例子可以看出,这个委托只能存放一个没有返回值,有一个int 形参数的方法。
  2. 实例化这个委托,和其他引用对象一样,使用new创建。它需要一个参数,那就是要与此委托绑定的方法。
  3. 任何与委托的签名相同的方法,都可以绑定到他上面。(具 体怎么绑定后面介绍)

 

二 委托的作用

 

了解了什么是委托,以及如何建立一个委托我们已经知道了。但是委托有什么用呢?为什么要用委托呢? 前面提到了,委托是用来用来处理其他语言需用函数指针来处理的情况的。这是他作用之一,总的说来在.NET 平台上委托有三大作用:实现委托功能,实现回调函数功能,实现事件功能。

 

1:委托功能

 

委托不仅仅是一个对象,你还应该把他看做一种功能,刚开始的时候我也困惑于这种区别。把他当一个对 象来看时,他是用于封装函数指针。然后提供函数指针可以提供的各种功能(当然也包括委托的功能)。而把 他当作一种功能时就更好理解了。比如日常生活做,你有些事情不会做,或不想做,你可以委托其他人帮你做 。这就是委托的功能。而在我们程序里表现的就是,比如一个下载程序,他有UI类和download两个类。在 download类中我想边下载边显示进度,但为了减少耦合,我不希望在download类中实现显示,那么我可以通过 委托这个对象,使用委托功能,来委托UI类做显示的工作。有点扰,但我想我应该还是说明白了委托对象和委 托功能的关系。(刚开始接触委托,我就是无法弄清这2个关系,所以觉得很乱)

class download
{
   public void down( );  //下载的功能
   
  //我的 download想用绘画界面的功能
  //但我不自 己实现,而是委托UI类来实现
  //而我通过 委托去调用这个方法

 delegate void DelegatePrint( );  //定义一个和要调用方法相同签名的委托


 public static void Main()
 { 
    down(); 
    DelegatePrint dp = new DelegatePrint( new UI ().printUI);    //绑定方法
    dp ();      //调用委托,实际是调用了和委托绑定的方法(通过委托对象dp,实现了委托的功能 )
 }
 
} 


class UI
{
   public void printUI( );  //绘画界面的功能
}

上面就是一个委托功能的实现,当然你也可以在download中建一个UI对象,然后直接调用他的printUI的方 法。所以这个用法并不常见。但是可以通过使用委托,进行异步的处理,也就是让下载和绘画异步进行,这样 就不会出现下载过程中界面失去响应的问题,只有在这个时候,在一个函数中用委托的方式使用另一个函数才 有意义。关于多线程和异步调用,在后面专门文件介绍。

而通常情况,委托功能是提供给某个类,但他无法实现某个功能的时候,通过委托去调用其他的类的功能 。比如有一个容器类,他负责存放2个对象,并比较这两个对象。当我们设计这样一个类的时候,我门无法知 道它以后将要存储什么类型的数据,我们也没有必要知道。那么我们如何实现未知类型对象的比较呢?

用重载?用泛行?是个办法,但是如果存放的是用户定义的类型呢?难道我们能预测用户的类型?是的, 看起来好象没有办法做下去了。但你是否想到了委托?让别人帮我进行比较。让谁呢?当然是要存放到容器的 对象自己。我们只需在容器类中提供一个比较函数的委托,我们不需要知道,也无法知道运行时的数据类型。 但我们可以把比较的方法委托给数据类型对象自己去做。也就是调用数据对象中的比较的函数。

class Container
{
    private Object ob1,ob2;  //存放2个对象

    public void Container(Object o1,object o2 )
   {
       ob1 = o1;
       ob2 = o2;
   }

 //  public int compare(Object o1,object o2 )
 //   {
 / /    //不知道如何比较这2个对象
 //  } 

  //通过委托 ,那么存放在容器中的对象必须提供一个与此想匹配的方法供调用
   delegate int compareDelegate (Object o1,object o2 )
  
 //容器类的比较方法。参数是一个委托,他指向了容器中存放的对象自己实现的比较方法
 //而容器类并不需要知道比较对象的类型,如果你愿意,你甚至可以让这个比较方法执行其他操作
    //这完全由你决定,因为可以绑定任意匹配的方法 
  public void comapreObject(compareDelegate cm)
 {
    if(cm != null)    
          cm(ob1,ob2);    
 }
} 


class UserType( )
{
   private int x ,y;
   public void UserType( int x,int y)
  {
     this.x =x;
     this y =y;
  }

 public static int compare(UserType a,UserType b )
 { 
    //比较大小
 }

} 


class Test
{
   statoc void Main()
   {
      //创建2个用户对象
       UserType  u1 = new UserType(1,2);
       UserType  u2 = new UserType(1,3);
      //把2个对象存放到容器
       Container cn = new Container(u1,u2);
        //下面要执行容器对象的比较,没有调用public int compare() 这个方法,因为这个方法无法知道如何比较
      
         //我们来看看使用委托实现运行时指定方法:
         //实例化一个委托,绑定要比较的对象所提供的比较方法(在这里确定这个委托指向那么个方法)
      Container.comapreObject  cd &n bsp;=  new Container.comapreObject(UserType.compare); 
      //调用容器类的比较方法,传递一个委托参数(运行是执行委托的方法->UserType.compare)
       cn.comapreObject(cd);
   }
}

看到上面的程序,容器类的比较方法,并没有自己去实现比较,内部而是通过委托去调用了其他的方法。也就是说他在编译时并不知道自己要干什么,而是在运行时,根据委托绑定的方法和对象,自己去找实现比较的方法。这就实现了运行时去确定一个方法。这里意思就是,定义一个委托,告诉容器类,通过我去找方法。而到底找什么方法,那就是我们自己来定了。

注意:上面的容器类应该用范性来定义,否则委托的参数类型是不匹配的。

看了委托对象的委托功能,应该了解在什么时候使用委托功能了,主要是在异步调用和编译时无法确定要运行的方法时使用。下面看一下委托另外两种最重要的功能

 

2:通过委托实现事件功能

 

关于事件的介绍请看  .NET学习笔记(六) ——事件  

关于事件我这里在进行一下总结,事件其实是对WINDOWS消息机制的一种封装。我们都知道WINDOWS系统是 基于驱动的。你点一个窗体,系统就向这个窗体发送一个消息。而在.NET中则被封装为了一个click事件。他 的含义就是我在点窗体的时候,通知窗体,然后窗体进行响应。

用个例子来说就是:电灯和开关的关系。我的电灯只负责发光,而开关只负责开关电源。为什么开了开关 电灯就会亮?因为开关向电灯发送了一个消息,告诉电灯,我把电源打开了。他只负责通知,而并不需要知道 他开了电源,电灯会干什么,当然他也不可能知道。而电灯作为一个接受者,他只接受到消息后就来做自己想 做的时,是选择亮,不亮,还是烧毁自己呢?

那说到这里和委托有什么关系?委托就是实现两者之间联系的纽带,就好比电线。没有电线,电流不知道 往那跑。而且一个开关可以控制多个电灯。这也就是常说的多播委托。就好比程序中你点一个安妞,可以触发 多个事件。

下面还是举个例子

//定义一个开关类
class Switch 
...{
   

    //委托类型定义了接受者必须实现的回调方法原型
    public delegate void SwitchEventHandler (Object sender, EventArgs args);

    //定义一个委托对象
    public  SwitchEventHandler  SwitchOn;


    //定义一个方法通知电灯
    protected virtual void OnSwitchOn(EventArgs e)
    ...{
        //有对象登记事件?
        if(SwitchOn!= null)
        ...{
            //如果有,则通知委托链表上的所有对象 (电线上的所有电灯)
            SwitchOn (this,e);
        }

    }

    //调用此方法,触发事件
    public void pushSwitch( )
    {
   
        OnSwitchOn(e);
    }

}
//定义电灯类
class Light
...{
    public Light(Switch s)
   {

        s.SwitchOn += new SwitchEventHandler(TurnOn)  //绑定方法 

    }


  private void TurnOn(Object sender,EventArgs e)
  {
        Console.WriteLine("turn on");
    }
 

} 

class Test()
{
   public static voId Main()
   {
       Switch sw = new Switch();
       Light  L1 = new Light(sw);
       Light  L2 = new Light(sw);
       sw.pushSwitch();
        
   }
}

上面定义了一个开关类,里面有一个委托,他和电灯类的方法是相同的签名。所以可以用来装电灯的方法 。在Light类的构造函数中,把委托和电灯的turnOn方法绑定起来,相当于把电灯和开关用电线(委托)连接 起来了。当我执行了SWitch的pushSwitch操作,就触发了SwitchOn这个事件。于是开关通过委托告诉电灯,我 被按了,而且是通知所有在委托上的电灯,这里是2个。

而这个时候电灯收到消息,就会开始执行相应的方法。这个方法是在Light构造函数中通过和委托绑定方法 指定的。你也可以把他和其他的方法绑定。这些都是自己Light决定的。而开关只负责告诉你,我被按了,具 体你执行什么操作,就在这里设置。

程序有问题?那里?

为什么Light中绑定方法使用的是‘+=’而不是‘=’???恩,这是个问题,但不 是这里的问题。前面说了委托可以装函数,而且可以装多个函数。他就好象链子一样,把自己绑定的方法连起 来。就好象电线把电灯都连起来一样。关于委托链我们后面介绍。这里我们要注意的是下面的问题

仔细的朋友一定看到了:

 //定义一个委托对象
 public  SwitchEventHandler  SwitchOn;

是啊,我这里定义的是一个委托而不是一个事件。那程序为什么也正常的。既然正常,我们为什么还要事 件呢?事件和委托到底是什么关系呢?一些列的问题又来了,下面就一一回答。

先给出事件中正确的定义方法:

//定义一个委托对象
public  event SwitchEventHandler  SwitchOn;

两者的差别就在于这一个event关键字,加了一个event他到底做了什么,大家可以看前面事件那片文章, 里面有详细的介绍。用event他产生了3个构造方法,其中SwitchOn成了私有类型,而通过add和remove方法对 他进行操作,这就意味着,我们不能对SwitchOn直接进行操作了。那和上面没event的定义有什么区别呢?

如果没有event,就不回产生3个构造方法,SwitchOn就是公有的,那会出现什么情况?我可以直接对 SwitchOn进行设置。如果我不小心把上面Light的构造函数写成了

public Light(Switch s)
{
     s.SwitchOn = new SwitchEventHandler(TurnOn)  //绑定方法 
}

这个有错吗?没错,委托就是这么绑定方法的。但这样的结果是什么。委托上只会绑定这一个方法。我就 无法把多个方法绑定到委托上,委托上永远只有这一个方法。那么事件发生时我就无法通知所有的注册的对象 了。而用了evevt关键字,我们就只能对他进行add,remove操作了。这也符合的事件的设计初衷。

事件我们就介绍到这里,我想通过了解委托,大家应该对事件了解的更清楚了。而事件还用到了委托链。 我们在下一篇问文章中介绍。

 

 

3:回调函数功能

 

什么是回调函数?网上也有各种对回调函数的定义。比较通俗的理解就是:一个函数被当作一个变量传递给一个方法,这个方法在执行是会使用到这个函数,那么这个函数就被叫做回调函数。而传递这个函数的地址是通过函数指针传递的。那么在.NET平台中,我们就是通过传递一个委托,来实现回调函数的功能。

那么回调函数到底有什么用呢?还记得上面一个比较对象的例子吗?我们本是想调用容器类的方法来比较自定义对象的大小,但容器类实际却调用了我们自定义对象的方法来进行比较。被调用者调用调用者提供的函数的过程就叫回调。

操作系统拥有更广泛意义上的回调,那就是消息机制。消息本是 Windows 的基本控制手段,乍看与函数调用无关,其实是一种变相的函数调用。发送消息的目的是通知收方运行一段预先准备好的代码,相当于调用一个函数。比如程序中使用了一个系统定时器,在程序中调用了这个定时器的方法,而当到了指定时间,定时间器会给程序发消息,要我执行我的一断代码。这就是广意上的回调。这更多的用在C/C++中,而在.NET中,这种消息机制被进行了封装,成了事件event。这里不过多的谈论回调函数,具体可以参见文章:关于回调函数 ,这里说的比较详细,但是我还是看的很晕。下面我们主要看看在.NET中如何通过委托实现回调函数。

系统中有许多API函数需要我们提供一个回调方法,下面是MSDN上的一个例子:

using System;
using System.Collections.Generic;
using System.Text;
using System.Runtime.InteropServices;

namespace MyDelegate
...{
    委托实现回调函数#region 委托实现回调函数

    public delegate bool CallBack(int hwnd, int lParam);

    public class EnumReportApp
     ...{
         [DllImport("user32")]
        //user32.dll中的Win32函数原型为:
        //BOOL EnumWindows(WNDENUMPROC lpEnumFunc, LPARAM lParam)
        //表示此函数需要回调的线索之一是存在 lpEnumFunc 参数。如果参数采用指向回调函数的指针,其名称中通常会有 lp(长指针)前缀与 Func 后缀的组合。
        //这里在导入user32.dll后,引入该函数,但是已经将其第一个参数传入了这里定义的委托函数CallBack
        public static extern int EnumWindows(CallBack x, int y);

        public static bool Report(int hwnd, int lParam)
         ...{
             Console.Write("Window handle is ");
             Console.WriteLine(hwnd);
            return true;
         }

        public static void Main()
         ...{
            //实例化一个委托,其实现为上面定义的Report,即调用myCallBack时就是调用了Report
             CallBack myCallBack = new CallBack(EnumReportApp.Report);
            //从这个.NET程序中调用外面user32.dll中的Win32的函数,但是该函数“向回调用(回调)”刚刚定义的.NET程序中的函数myCallBack
             EnumWindows(myCallBack, 0);
             Console.ReadLine();
         }
     }

    #endregion
}

可以看到,使用的API函数 EnumWindows(WNDENUMPROC lpEnumFunc, LPARAM lParam),他第一参数表示需要传递一个函数给他。而传递函数地址在.net中通过委托来实现,于是我们定义了一个委托,把他和要传递的函数Report绑定。然后把委托传递给API函数,这样就实现了一个回调函数的传递。

上面是在托管平台调用非托管DLL中需要回调函数方法的情况,当然还有前的事件,也需要回调功能,还有在委托功能做提到过的异步调用。委托、接口和事件允许提供回调功能。每个类型都有自己特定的使用特性,使其更适合特定的情况。

 

事件

 

如果下列条件为真,请使用事件:

  • 方法预先注册回调函数,一般通过单独的 AddRemove 方法。
  • 一般情况下,一个以上的对象需要事件通知。
  • 希望最终用户能够轻松地将侦听器添加到可视化设计器的通知中。

 

委托

如果下列条件为真,请使用委托:

  • 需要 C 语言样式的函数指针。
  • 需要单个回调函数。
  • 希望注册在调用中或构造时发生,而不是通过单独的 Add 方法。

接口

如果回调函数要求复杂的行为,请使用接口。

 

 

三 小结

 

 

通过上面的内容对委托的功能和使用应该有一定了解。简单来说委托就是一个容器,用来存放函数的地址。委托对象可以当参数传递,于是也就传递了函数。因为WINDOWS是基于消息机制的,消息机制是一种广义的回调,所以关键的关键是回调函数。在C/C++中 函数指针来实现回调函数功能,然后应用到消息机制上去。而在.NET中,委托是对函数指针的封装,事件是对消息的封装。所以在.NET中委托的功能是实现了回调函数(说是封装回调函数更便于理解),然后又通过实现的回调函数实现了事件等操作。说到这里已似乎成了讨论消息机制和回调函数了。而我对这也不了解,所以无法在说下去。还是回到.NET的委托上把。知道可以用委托来传递函数,实现事件模型已经异步调用就可以了。

在下一章将进一步介绍委托的内部实现,他是如何绑定一个方法,并且在运行时找到这个方法并执行的,他是如何同时绑定多个方法的,事件中如何合理的设置委托。

 


如果本文对您有帮助,可以扫描下方二维码打赏!您的支持是我的动力!
微信打赏 支付宝打赏

1 评论

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注