bindsang

工作五年,长期从事于asp.net方面的编程,业余爱好VC编程,温和、谦虚、自律、自信、善于与人交往沟通
  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

谈谈C#中的Delegate

Posted on 2008-07-28 10:14    阅读(2227)  评论(5编辑  收藏  举报
       本人现在长期从事于.NET下的开发,因为工作的关系,间断的做过一些C++,DELPHI的程序,对后两者的程序语言有一定的了解,因此在平时也经常遇 到有人问我关于C#和另外两种语言相比有哪些特点。我所了解的其中很大一个特点就是C#里没有指针,所有的对象全部通过引用来访问。引用的类型之间不能随 便进行转换,避免了程序运行过程中出现的转换可能发生的问题(例如把一个整型转成一个指针进行访问等类似的潜在危险)。普能对象可以通过指针来引用,可是 对于C++,DELPHI中的函数指针作为参数传送这样的函数调用形式在C#里面又应该怎么来表示呢。答案就是Delegate。同C++/DELPHI 用函数指针来实现事件(Event)和回调函数类似,在C#中是通过Delegate来实现。

       首先来看看Delegate的中文句称是什么,网上找得到的翻译有好几种名称,委托、委派、指派,代理等。个人理解:“代理”好像是个专有名称,在设计模 式和分层开发中有特定的含意,容易引起误解;“指派”,“委派”有点象个动词,不适合来命名,读起来总觉得那么别扭,好像港台地区用得比较多一点;“委 托”读起来感觉有点文皱皱的,但是 MSDN的官方翻译是“委托”,没办法,谁叫MSDN比较权威呢,在没有找到更好的名称之前,我还是比较倾向于使用“委托”这个名称的,所以以下内容均采 用这一中文命名。

      再来说说委托本身吧,在C#里面委托不再是像C++/DELPHI那样就是一个简单的整型值,而且一个有着复杂内部逻辑的数据结构体,具体的来说就是一个 类(class),只不过这个类有点特殊,个人感觉有点像重载了()运算符的C++类对象,只不过C++类中重载()运算符只是它的一个附加功能,而C# 中委托只能用于作函数的封装,调用。

// 声明一个委托
public delegate void AlarmEventHandler(object sender, AlarmEventArgs e);

        在定义的时候不是像普通类那样能够再声明内部数据,函数等;在声明的时候没有基类的声明,所有的委托都隐式的从 MulticastDelegate(继承自Delegate) 这个抽象的基类继承下来;所有的声明的委托都自动是sealed的,不能够再次被继承;实例化一个委托的时候只能够指定一个函数名作为参数(这也是可以理 解的,委托嘛,最初设计的目的就是用来包装函数的);C#里的委托和C++/DELPHI最大的不同就是既可以传递静态函数,也可以传递非静态函数,没有 试过能不能传递extern的函数,因为extern的函数都可以用一个静态函数再传递调用一下。如果有谁试过,告请告知一下结果(虽然DELPHI里面 有一种函数指针是在普通函数指针的声明后面加上 of object可以变成指向类成员函数的指会,但是在形式上这两种函数指针类型不能像C#那样完美的统一在一起)。这都得益于委托是个数据结构体,可以有额 外的存储区保存非静态函数的实例对象的信息,通过委托对象的Target属性可以访问到,如果代表的是一个静态函数的委托,该属性值为null。不过这也 带来了一些负面影响,使得很多从C#开始学编程的人在使用委托很长的时间里都不知道引用实例成员函数和引用类的静态函数在原理上是完全不一样的,不知道为 什么C++的回调函数都要声明成静态的或者不属于任类的。

        与函数指针相比,委托是面向对象、类型安全、可靠的托管(Managed)对象。也就是说,CLR能够保证委托指向一个有效的函数,你无须担心委托会指向 无效地址或者越界地址。借用MSDN中的一句话-------由于它们与其他编程语言中所使用的函数指针相似,因此它们有时也被称为“类型安全的函数指 针”。

       委托的比较。
       根据MSDN的说明,按照以下方式比较这些函数和目标是否相等:
    
     * 如果所比较的两种函数都是静态的且对同一类为同一函数,则这些函数被视为相等,这些目标也被视为相等。
     * 如果所比较的两种函数都是实例函数且对同一个对象为同一种函数,则这些函数被视为相等,这些目标也被视为相等。
     * 否则,这些函数被视为不相等,这些目标也被视为不相等。

       如果两个调用列表有相同的顺序,并且两个列表的对应元素表示相同的函数和目标,则认为这两个调用列表相同。
       在 .NET Framework 1.0 和 1.1 版中,如果两个委托的目标、函数和调用列表都相等,则即使这两个委托的类型不相同,它们也被视为相等。

       也是因为委托是一个数据结构体,使得它具备了一些原本C++/DELPHI所不具备的特点和功能,具体来说有如下几个功能:
        运行时多态
         因为所有的委托最终都继承于Delegate类,再加上面向对象本身有拥有的运行时多态的特性,使得委托也具有这个特性。
        我们知道在其它语言中函数指针本质上是一个整型,只是这个值指向一类和函数指针具有相同签名的函数的入口地址,没有附加的信息来描述这个 指针,所以函数指针一旦赋值后不能随便改变,也不能随便转换。这使得在编程的时候只能提供具有特定签名的函数指针的参数,一旦函数指针失去了具体的函数指 会类型,比如强制转换一个函数指针类型到void*,虽然值还是代表了原来那个函数的入口地址,但是该函数指针再也没有办法正常调用了,因为它失去了函数 签名的信息。
       委托与这不一样,因为委托本质上是个类,每个委托内部都存有当前委托的签名,相关联的函数,函数所在的类的实例(如果有的话),在把某个具体的委托对象传 给Delegate的引用的时候,这些信息都不会丢失,这时使用委托的Invoke成员函数可以实现多个不同的委托用同一种形式调用,也就达到了多态的目 的,当然因为传递的参数有可能不相同,就使得这种多态有一定的局限性。一个比较典形的应用是Control类的Invoke方法,非UI线程调用 Control的函数传递的参数就是Delegate和它需要的参数数组。

        多路广播
        多路广播指的一个委托的调用列表中可以拥有多个元素的委托,内部是通过一个带有链接的委托列表来存储所有的被引用的单个委托,在调用多路广播委托时,将按 照调用列表中的委托出现的顺序来同步调用这些委托,有点类似于Observer模式。顺便提一下,如果在该列表的执行过程中发生错误,则会引发异常,如果 相那时避免这种情况发生的话可以在绑字的函数中处理所有可能发生的异常,或者是用OneWayAttribute修饰绑定的函数,告诉CLR不向外抛出异 常。事件委托就是一个多路广播,这意味着我们可以对多个事件处理函数进行引用。

// 声明一个AlarmEvent事件,事件类型为AlarmEventHandler
public event  AlarmEventHandler AlarmEvent

        我们知道在C#事件中使用+=运算符来绑定一个事件,使用-=运算符来移除一个件,同一个委托可以多次加入到对应的事件中,这意味着只要该事件引发一次, 对应的事件处理函数就可以被调用多次。移除事件委托的时候只需要判断是否存在绑定到给定函数上的委托,而不是比较两个委托引用是代表同一个委托对象。如果 发现有绑定到同一个函数上的委托就移除,如果存在多个符合的委托,一次只会移除一个。
       在某些情况下,我们不一定是用事件的方式来使用多路广播,而是使用函数调用的方式来完成的,在Delegate类里面有个Combine函数,可以把一个或多个委托合并成一个新的委托,对新的委托的调用可以分别应用到每个合并前的委托调用上。
       对多路广播的几个补充说明:有几个在MSDN上没有提到的,但是隐式的需要遵守的规则。一般的委托的声明中对于返回类型没作特别要求,但是多路广播中对应 的每个委托应该都是无返回类型的,这个也好理解,如果存在返回值的话,到底是返回哪一个委托的返回值呢。多路广播中组成 MulticastDelegate 的单个委托的签名(声明)应该是一致的,否则调用的时候会出现运行时错误。

       异步调用
       使用一个特定类型的委托对象很容易的实现异步调用,因为在编译每个特定类型的委托类的时候编译器自动加入了BeginInvoke和EndInvoke两 个函数,借助于Reflector之类的工具可以看到这两个函数没有函数体,只有一个签名和 [MethodImpl(0, MethodCodeType=MethodCodeType.Runtime)]特性(Attribute),在调用这两个函数的时候,CLR自动在内 部实现了异步调用的所有事情,这给程序员在多线程编程中省了很多事。

啰嗦了这么多,总结一下吧。

实现一个Delegate是很简单的,通过以下3个步骤即可实现
1. 声明一个Delegate对象,它应当与你想要传递的函数具有相同的参数和返回值类型。
    声明一个委托的例子:
    public delegate int MyDelegate(string message);

2. 创建delegate对象,并将你想要传递的函数作为参数传入。
     创建委托对象的函数:
    1). MyDelegate myDelegate = new MyDelegate(实例名.函数名);
    2). MyDelegate myDelegate = new MyDelegate(类名.函数名);

注:如果需要委托的函数是一个static静态函数的话,采用第2种方式,否则采用第1种方式。

3. 在要实现异步调用的地方,通过上一步创建的对象来调用函数。
    可以直接使用委托调用委托所指向的函数:
    myDelegate(向函数传递的参数); // 这点和C++/DELPHI中函数指针的用法挺相似的

下面是一些需要注意的事情:
"委托"(Delegate)(代表、委托):“委托”是类型安全的并且完全面向对象的。
(1)在C#中,所有的委托都是从System.MulticastDelegate类派生的,这点由编译器保证。
(2)委托隐含具有sealed属性,即不能用来派生新的类型。
(3)多路委托最大的作用就是为类的事件绑定事件处理程序。
(4)在通过委托调用函数前,必须先检查委托是否为空(null),若非空,才能调用函数。
(5)在委托实例中可以封装静态的函数也可以封装实例函数。
(6)在创建委托实例时,需要传递将要映射的函数或其他委托实例以指明委托将要封装的函数签名/原型(.NET中称为函数签名:signature)。如果映射的是静态函数,传递的参数应该是类名.函数名,如果映射的是实例函数,传递的参数应该是实例名.函数名。
(7)只有当两个委托实例所映射的函数以及该函数所属的对象都相同时,才认为它们是想等的(从函数地址考虑)。
(8)多个委托实例可以形成一个委托链,System.Delegate中定义了用来维护委托链的静态函数Combion,Remove,分别向委托链中添加委托实例和删除委托实例。
(9)因为委托是一种类型,所以委托的定义可以放在任何类的外面,作为顶级类型,也可以放到其它类型中作为内嵌类型(Nested)。
(10)在与其它语言如C++/DELPHI开发的DLL交互的时候,在PInvoke调用的时候完全可以把委托等同于函数指针