简介
       委托是C#中的一种引用类型,类似于C/C++中的函数指针。与函数指针不同的是,委托是面向对象、类型安全的,而且委托可以引用静态方法和实例方法,而函数指针只能引用静态函数。委托主要用于 .NET Framework 中的事件处理程序和回调函数。
       一个委托可以看作一个特殊的类,因而它的定义可以像常规类一样放在同样的位置。与其他类一样,委托必须先定义以后,再实例化。与类不同的是,实例化的委托没有与之相应的术语(类的实例化称作对象),作为区分我们将实例化的委托称为委托实例。
函数指针
       一个函数在编译时被分配给一个入口地址,这个入口地址就称为函数的指针,正如同指针是一个变量的地址一样。
函数指针的用途很多,最常用的用途之一是把指针作为参数传递到其他函数。我们可以参考下面的例子进一步理解函数指针作为参数的情况:
# include<stdio.h>
int max(int x,int y)
{
       return (x>y?x:y);
}
int min(int x,int y)
{
       return(x<y?x:y);
}
int sub(int x, int y)
{
       return(x+y);
}
int minus(int x,int y)
{    
       return(x-y);
}
void test(int (*p)(int,int),int (*q)(int,int),int a,int b)
{
       int Int1,Int2;
       Int1=(*p)(a,b);
       Int2=(*q)(a,b);
       printf("%d,\t%d\n",Int1,Int2);
}
void main()
{
       test(max,min,10,3);
       test(sub,minus,10,3);
}
客观的讲,使用函数指针作为其参数的函数如果直接调用函数或是直接把调用的函数的函数体放在这个主函数中也可以实现其功能。那么为什么还要使用函数指针呢?我们仔细看一下上面的main()函数就可以发现,main()函数两次调用了test函数,前一次求出最大最小值,后一次求出两数的和与差。如果我们 test函数不用函数指针,而是采用直接在test函数中调用函数的方法,使用一个test函数还能完成这个功能吗?显然不行,我们必须写两个这样的 test函数供main()函数调用,虽然大多数代码还是一样的,仅仅是调用的函数名不一样。上面仅仅是一个简单的例子,实际生活中也许main()函数会频繁的调用test(),而每次的差别仅仅是完成的功能不一样,也许第一次调用会要求求出两数的和与差,而下一次会要求求出最大值以及两数之和,第三次呢,也许是最小值和最大值,……,如果不用函数指针,我们需要写多少个这样的test()函数?显然,函数指针为我们的编程提供了灵活性。
另外,有些地方必须使用到函数指针才能完成给定的任务,特别是异步操作的回调和其他需要匿名回调的结构。另外,像线程的执行,事件的处理,如果缺少了函数指针的支持也是很难完成的。
类型安全
从上面的介绍可以看出,函数指针的提出还是有其必要的,上面的介绍也同时说明了委托存在的必要性。那么为什么C#中不直接用函数指针,而是要使用委托呢?这就涉及到另外一个问题:C#是类型安全的语言。何谓类型安全?这里的类型安全特指内存类型安全,即类型安全代码只访问被授权可以访问的内存位置。如果代码以任意偏移量访问内存,该偏移量超出了属于该对象的公开字段的内存范围,则它就不是类型安全的代码。显然指针不属于类型安全代码,这也是为什么C#使用指针时必须申明unsafe的缘故。
那么类型不安全代码可能会带来什么不良的后果呢?相信对于安全技术感兴趣的朋友一定十分熟悉缓冲区溢出问题,通过缓冲区溢出攻击者可以运行非法的程序获得一定的权限从而攻击系统或是直接运行恶意代码危害系统,在UNIX下这是一个十分普遍的问题。那么缓冲区溢出又和函数指针有什么关系呢?事实上,攻击者就是通过缓冲区溢出改变返回地址的值到恶意代码地址来执行恶意代码的。我们可以看看下面的代码:
void copy()
            {
                 char buffer[128];
                 ........
                     strcpy (buffer,getenv("HOME"));//HOME为UNIX系统中的HOME环境变量
                 ........
            }
上面的代码中如果HOME环境变量的字符数大于128,就会产生缓冲区溢出,假如这个缓冲区之前有另一个函数的返回地址,那么这一是地址就有可能覆盖,而覆盖这一地址的字符有可能就是恶意代码的地址,攻击者就有可能攻击成功了!
上面的例子仅仅是指针问题中的一种,除此以外,还可能由于错误的管理地址,将数据写入错误地址,造成程序的崩溃;还可能由于对指针不恰当的赋值操作产生悬浮指针;还可能产生内存越界,内存泄漏等等问题。
由此可见,指针不是类型安全的,函数指针当然也不例外,所以C#里面没有使用函数指针,而且不建议使用指针变量。
委托
前面的说明充分证明了委托存在的必要性,那么我们再谈谈为什么委托是类型安全的。C#中的委托和指针不一样,指针不通过MSIL而是直接和内存打交道,这也是指针不安全的原因所在,当然也是采用指针能够提高程序运行速度的缘故;委托不与内存打交道,而是把这一工作交给CLR去完成。CLR无法阻止将不安全的代码调用到本机(非托管)代码中或执行恶意操作。然而当代码是类型安全时,CLR的安全性强制机制确保代码不会访问本机代码,除非它有访问本机代码的权限。
委托派生于基类System.Delegate,不过委托的定义和常规类的定义方法不太一样。委托的定义通过关键字delegate来定义:
public delegate int myDelegate(int x,int y);
上面的代码定义了一个新委托,它可以封装任何返回为int,带有两个int类型参数的方法。任何一个方法无论是实例方法还是静态方法,只要他们的签名(参数类型在一个方法中的顺序)和定义的委托是一样的,都可以把他们封装到委托中去。这种签名方法正是保证委托是类型安全的手段之一。
产生委托实例和产生类实例(对象)差不多,假如我们有如下的方法:
public int sub(int x,int y)
{
       return(x+y);
}
我们就可以使用如下的代码得到一个委托实例:
myDelegate calculatin=new myDelegate(sub);
接下来我们就可以直接使用calculation调用sub方法了:
calculation(10,3);
下面我们将用委托重写上面的一个程序来看一下在C#中如何通过委托实现由函数指针实现的功能:
using System;
class MathClass
{
       public static int max(int a,int b)
       {
              return(a>b?a:b);
       }
       public static int min(int a,int b)
       {
              return(a<b?a:b);
       }
       public static int sub(int a,int b)
       {
              return (a+b);
       }
       public static int minus(int a,int b)
       {
              return (a-b);
       }
}
class Handler
{
       private delegate int Calculation(int a, int b);
       private static Calculation[] myCalculation=new Calculation[2];
       public static void EventHandler(int i,int a,int b)
       {
              switch (i)
              {
                     case 1:
                            myCalculation[0]=new Calculation(MathClass.max);
                            myCalculation[1]=new Calculation(MathClass.min);
                            Console.WriteLine(myCalculation[0](a,b));
                            Console.WriteLine(myCalculation[1](a,b));
                            break;
                     case 2:
                            myCalculation[0]=new Calculation(MathClass.sub);
                            myCalculation[1]=new Calculation(MathClass.minus);
                            Console.WriteLine(myCalculation[0](a,b));
                            Console.WriteLine(myCalculation[1](a,b));
                            break;
                     default:
                            return;
              }
       }
}
class Test
{
       static void Main()
       {
              Handler.EventHandler(1,10,3);
              Handler.EventHandler(2,10,3);
       }
}
 
我们还可以声明一个委托数组,就像声明一个对象数组一样,上面的例子中就使用到了委托数组;一个委托还可以封装多个方法(多路广播委托,经常与事件处理程序结合使用),只要这些方法的签名是正确的。多路广播委托的返回值一般为void,这是因为一个委托只能有一个返回值,如果一个返回值不为void的委托封装了多个方法时,只能得到最后封装的方法的返回值,这可能和用户初衷不一致,同时也会给管理带来不方便。如果你想通过委托返回多个值,最好是使用委托数组,让每个委托封装一个方法,各自返回一个值。
事件
在C#中,委托的最基本的一个用处就是用于事件处理。事件是对象发送的消息,以发信号通知操作的发生,通俗一点讲,事件就是程序中产生了一件需要处理的信号。
事件的定义用关键字event声明,不过声明事件之前必须存在一个多路广播委托:
public delegate void Calculate(int x,int y);//返回值为void的委托自动成为多路广播委托;
public event calculate OnCalculate;
从上节的委托实例和上面的事件的声明可以看出,事件的声明仅仅是比委托实例的声明多了个关键字event,事实上事件可以看作是一个为事件处理过程定制的多路广播委托。因此,定义了事件后,我们就可以通过向事件中操作符+=添加方法实现事件的预定或者是通过-=取消一个事件,这些都与委托实例的处理是相同的。与委托实例不同的是,操作符=对于事件是无效的,即
OnCalculate=new calculate(sub) ;//无效
只是因为上面的语句会删除由OnCalculate封装的所有其他方法,指封装了由此语句指定的唯一方法,而且一个预定可以删除其他所有方法,这会导致混乱。
回调函数
回调函数是在托管应用程序中可帮助非托管 DLL 函数完成任务的代码。对回调函数的调用将从托管应用程序中,通过一个 DLL 函数,间接地传递给托管实现。在用平台调用调用的多种 DLL 函数中,有些函数要求正确地运行托管代码中的回调函数。关于回调函数只是使用到委托,在此不加过多说明,具体实现可参考下图:

 

 

一、什么是委托

      下面引用自 MSDN

      委托类型声明的格式如下:

public delegate void TestDelegate(string message);


      delegate 关键字用于声明一个引用类型,该引用类型可用于封装命名方法或匿名方法。委托类似于 C++ 中的函数指针;但是,与函数指针不同,委托是面向对象和类型安全的。

      通过将委托与命名方法或匿名方法关联,可以实例化委托。与之关联的匿名方法必须除了方法名之外参数类型、参数个数、参数顺序和返回值都必须和声明的委托类型保持一致。
对于可隐式转换的参数和返回值类型处理可查阅 MSDN。

       看看《.NET 大局观》第2版中对委托的定义:委托是指向方法的一个安全可靠的指针。所有 delegate 都继承自一个共同的 System.Delegate 类型,通常用于事件的处理和回调(callbacks)。每个委托都关联一系列成员,称为调用列表(invocation list)。一旦委托被调用,列表中的每一个成员也都会被调用,并获得委托所收到的参数。

二、委托是什么?

      上面已经提到了什么是委托,现在又反问了委托是什么,这不是吃饱饭没事做忽悠人吗?
      当然不是,上面的问题是从委托本身狭小的范围来说事的,现在是要在超出委托定义的范围来说明。

      为了看清委托的真面目,我们来创建一个委托类型

namespace DelegateAndEventTest
{
    public delegate void SampleDelegate(string message);
}


      再用 ildasm 打开编译后的 dll 查看生成的 IL 代码,结果如下:

      

      没有搞错,委托其实也只是一个类,它派生自 System.MulticastDelegate。

       既然委托是一个类,那它实例化出来的当然也是一个对象,它存储了一个类的方法的引用。这点对于用惯了 .NET 或者 JAVA 的用户来说不是很好理解,因为在他们的潜意识中已经把方法当成了语言的语法特性,它们可以被定义,被调用,但却不是数据类型,其实这样理解也是正确无误的,但却对我们理解方法引用造成一些障碍,直到有一天看《JS 权威指南》中对函数的介绍时忽然茅塞顿开,因为在 js 中它把函数也当成一种数据类型处理,如果在委托中也把方法当成一种数据类型看待呢?一个方法同样同样也是需要被分配一定范围内存空间的,同样也可以通过地址访问,委托存储的就是它的地址引用。

      总结:委托是一个定义签名的类型,即方法的返回值类型和参数列表类型。可以使用委托类型来声明一个变量,该变量可以引用与委托签名相同的所有方法。

      C#高级编程(第4版)中提到:理解委托的一种好方式是把委托的作用当作是给方法签名指定名称。

三、什么是事件

      下面的解释来自MSDN,基本上已经能说明事件的概念了:

      .NET 使用 event 关键字来指定事件。

      事件是类在发生其关注的事情时用来提供通知的一种方式。例如,封装用户界面控件的类可以定义一个在用户单击该控件时发生的事件。控件类不关心单击按钮时发生了什么,但它需要告知派生类单击事件已发生。然后,派生类可选择如何响应。
事件使用委托来为触发时将调用的方法提供类型安全的封装。委托可以封装命名方法和匿名方法。

      事件具有以下特点:
      * 事件是类用来通知对象需要执行某种操作的方式。
      * 尽管事件在其他时候(如信号状态更改)也很有用,事件通常还是用在图形用户界面中。
      * 事件通常使用委托事件处理程序进行声明。
      * 事件可以调用匿名方法来替代委托。

       事件处理程序委托的标准签名定义一个没有返回值的方法,其第一个参数的类型为 Object,通常命名为 sender,它引用引发事件的实例,第二个参数从 EventArgs 类型派生,通常命名为 e,它保存事件数据。如果事件不生成事件数据,则第二个参数只是 EventArgs 的一个实例。否则,第二个参数为从 EventArgs 派生的自定义类型,提供保存事件数据所需的全部字段或属性。

四、 一个关于委托与事件的例子

       下面是一个关于一个人从一个地方走到另一个地方的例子,Person 中包含一个方法 Move(),同时 Move() 会触发两个事件,一个是 OnBeginMove (在开始移动时发生),另一个是 OnEndMove (在到达目的地时发生),为了使其更像一个人,我们给它加上一个 Name 属性。

Person 类源代码:

using System;

namespace DelegateAndEvent
{
    /**//// <summary>
    /// 声明一个委托,用于代理一系列"无返回"及"不带参"的自定义方法
    /// </summary>
    /// <param name="sender">事件源</param>
    /// <param name="e">不包含任何事件数据的 EventArgs</param>
    public delegate void MyEventHandler(object sender, EventArgs e); 

    /**//// <summary>
    /// 人类
    /// </summary>
    public class Person
    {
        /**//// <summary>
        /// 在开始移动时发生
        /// </summary>
        public event MyEventHandler OnBeginMove;

        /**//// <summary>
        /// 在到达目的地时发生
        /// </summary>
        public event MyEventHandler OnEndMove;

        private string _name;

        /**//// <summary>
        /// 名字
        /// </summary>
        public string Name
        {
            get { return _name; }
            set { _name = value; }
        }

        /**//// <summary>
        /// 移动
        /// <remarks>封装了触发事件的方法</remarks>
        /// </summary>
        /// <param name="place">目的地</param>
        public void Move(Place place)
        {
            // OnBeginMove 事件在这里被触发了
            if (OnBeginMove != null)
                OnBeginMove(this, EventArgs.Empty);

            OnMove(place);

            // OnEndMove 事件在这里被触发了
            if (OnEndMove != null)
                OnEndMove(this, EventArgs.Empty);
        }

        private void OnMove(Place place)
        {
            Console.WriteLine("我走啊走啊走啊走.");
            Console.WriteLine("我已经走到 x={0} y={1} 的位置", place.X, place.Y);
        }
    }
}


Place 类源代码:

using System;

namespace DelegateAndEvent
{
    public class Place
    {
        private int _x;
        private int _y;

        public Place() { }

        public Place(int x, int y)
        {
            this._x = x;
            this._y = y;
        }

        public int X
        {
            get { return _x; }
            set { _x = value; }
        }

        public int Y
        {
            get { return _y; }
            set { _y = value; }
        }
    
    }
}


客户端原代码:

using System;

namespace DelegateAndEvent
{
    public class Program
    {
        static void Main(string[] args)
        {
            // 创建一个 Person 的新实例
            Person Yyw = new Person();

            // 将事件与委托绑定
            // 这里使用了命名委托
            Yyw.OnBeginMove += new MyEventHandler(Yyw_OnBeginMove);
            // 这里使用了匿名委托(C# 2.0 的新特性)
            Yyw.OnEndMove += delegate(System.Object sender, System.EventArgs e)
            {
                Console.WriteLine("我已经走到了尽头");
            };
            
            Place place = new Place(10, 20);
            
            // 到那边去
            Yyw.Move(place);

            Console.Read();
        }

        static void Yyw_OnBeginMove(object sender, EventArgs e)
        {
            Console.WriteLine("我要开始走动了");
        }
    }   
}


程序输出结果:

我要开始走动了
我走啊走啊走啊走....
我已经走到 x=10 y=20 的位置
我已经走到了尽头

posted on 2007-06-28 16:56  Thunderdanky  阅读(336)  评论(0编辑  收藏  举报