委托

来源:互联网 发布:javascript 隐藏标签 编辑:程序博客网 时间:2024/04/29 05:17

当要把方法传送给其他方法时,需要使用委托。要了解它们的含义,可以看看下面的代码:

int i = int.Parse("99");

我们习惯于把数据作为参数传递给方法,如上面的例子所示。所以,给方法传送另一个方法听起来有点奇怪。而有时某个方法执行的操作并不是针对数据进行的,而是要对另一个方法进行操作,这就比较复杂了。在编译时我们不知道第二个方法是什么,这个信息只能在运行时得到,所以需要把第二个方法作为参数传递给第一个方法,这听起来很令人迷惑,下面用几个示例来说明:

       启动线程—— C#中,可以告诉计算机并行运行某些新的执行序列。这种序列就称为线程,在基类System.Threading.Thread的一个实例上使用方法Start(),就可以开始执行一个线程。如果要告诉计算机开始一个新的执行序列,就必须说明要在哪里执行该序列。必须为计算机提供开始执行的方法的细节,即Thread.Start()方法必须带有一个参数,该参数定义了要由线程调用的方法。

       通用库类—— 有许多库包含执行各种标准任务的代码。这些库通常可以自我包含。这样在编写库时,就会知道任务该如何执行。但是有时在任务中还包含子任务,只有使用该库的客户机代码才知道如何执行这些子任务。例如编写一个类,它带有一个对象数组,并把它们按升序排列。但是,排序的部分过程会涉及到重复使用数组中的两个对象,比较它们,看看哪一个应放在前面。如果要编写的类必须能给任何对象数组排序,就无法提前告诉计算机应如何比较对象。处理类中对象数组的客户机代码也必须告诉类如何比较要排序的对象。换言之,客户机代码必须给类传递某个可以进行这种比较的合适方法的细节。

       事件—— 一般是通知代码发生了什么事件。GUI编程主要是处理事件。在发生事件时,运行库需要知道应执行哪个方法。这就需要把处理事件的方法传送为委托的一个参数。这些将在本章后面讨论。

前面建立了有时把方法的细节作为参数传递给其他方法的规则。下面需要指出如何完成这一过程。最简单的方式是把方法名作为参数传递出去。例如在前面的线程示例中,假定要启动一个新线程,且有一个叫作EntryPoint()的方法,该方法是开始运行线程时的地方。

void EntryPoint()

{

   // do whatever the new thread needs to do

}

也可以用下面的代码开始执行新线程:

Thread NewThread = new Thread();

Thread.Start(EntryPoint);                   // WRONG

实际上,这是一种很简单的方式,在一些语言如CC++中使用的就是这种方式(CC++中,参数EntryPoint是一个函数指针)

但这种直接的方法会导致一些问题,例如类型的安全性,在进行面向对象编程时,方法很少是孤立存在的,在调用前,通常需要与类实例相关联。而这种方法并没有考虑到这个问题。所以.NET Framework在语法上不允许使用这种直接的方法。如果要传递方法,就必须把方法的细节封装在一种新类型的对象中,即委托。委托只是一种特殊的对象类型,其特殊之处在于,我们以前定义的所有对象都包含数据,而委托包含的只是方法的细节。

1  C#中声明委托

C#中使用一个类时,分两个阶段。首先需要定义这个类,即告诉编译器这个类由什么字段和方法组成。然后(除非只使用静态方法)实例化类的一个对象。使用委托时,也需要经过这两个步骤。首先定义要使用的委托,对于委托,定义它就是告诉编译器这种类型的委托代表了哪种类型的方法,然后创建该委托的一个或多个实例。

定义委托的语法如下:

delegate void VoidOperation(uint x);

在这个示例中,定义了一个委托VoidOperation,并指定该委托的每个实例都包含一个方法的细节,该方法带有一个uint参数,并返回void。理解委托的一个要点是它们的类型安全性非常高。在定义委托时,必须给出它所代表的方法的全部细节。

提示:

理解委托的一种好方式是把委托的作用当作是给方法签名指定名称。

假定要定义一个委托TwoLongsOp ,该委托代表的函数有两个long型参数,返回类型为double。可以编写如下代码:

delegate double TwoLongsOp(long first, long second);

或者定义一个委托,它代表的方法不带参数,返回一个string型的值,则可以编写如下代码:

delegate string GetAString();

其语法类似于方法的定义,但没有方法体,定义的前面要加上关键字delegate。因为定义委托基本上是定义一个新类,所以可以在定义类的任何地方定义委托,既可以在另一个类的内部定义,也可以在任何类的外部定义,还可以在命名空间中把委托定义为顶层对象。根据定义的可见性,可以在委托定义上添加一般的访问修饰符:public private protected等:

public delegate string GetAString();

注意:

实际上,“定义一个委托”是指“定义一个新类”。委托实现为派生于基类System. MulticastDelegate的类,System.MulticastDelegate又派生于基类System.DelegateC#编译器知道这个类,会使用其委托语法,因此我们不需要了解这个类的具体执行情况,这是C#与基类共同合作,使编程更易完成的另一个示例。

定义好委托后,就可以创建它的一个实例,以存储特定方法的细节。

注意:

此处,在术语方面有一个问题。类有两个不同的术语:“类”表示较广义的定义,“对象”表示类的实例。但委托只有一个术语。在创建委托的实例时,所创建的委托的实例仍称为委托。您需要从上下文中确定委托的确切含义。

2  C#中使用委托

下面的代码段说明了如何使用委托。这是在int上调用ToString()方法的一种相当冗长的方式:

private delegate string GetAString();

 

static void Main(string[] args)

{

   int x = 40;

   GetAString firstStringMethod = new GetAString(x.ToString);

   Console.WriteLine("String is" + firstStringMethod());

   // With firstStringMethod initialized to x.ToString(),

   // the above statement is equivalent to saying

   // Console.WriteLine("String is" + x.ToString());

在这段代码中,实例化了类型为GetAString的一个委托,并对它进行初始化,使它引用整型变量xToString()方法。在C#中,委托在语法上总是带有一个参数的构造函数,这个参数就是委托引用的方法。这个方法必须匹配最初定义委托时的签名。所以在这个示例中,如果用不带参数、返回一个字符串的方法来初始化firstStringMethod,就会产生一个编译错误。注意,int.ToString()是一个实例方法(不是静态方法),所以需要指定实例(x)和方法名来正确初始化委托。

下一行代码使用这个委托来显示字符串。在任何代码中,都应提供委托实例的名称,后面的括号中应包含调用该委托中的方法时使用的参数。所以在上面的代码中,Console.WriteLine()语句完全等价于注释语句中的代码行。

委托的一个特征是它们的类型是安全的,可以确保被调用的方法签名是正确的。但有趣的是,它们不关心调用该方法的是什么类型的对象,甚至不考虑该方法是静态方法,还是实例方法。

提示:

给定委托的实例可以表示任何类型的任何对象上的实例方法或静态方法—— 只要方法的签名匹配于委托的签名即可。

为了说明这一点,我们扩展上面的代码,让它使用firstStringMethod委托在另一个对象上调用其他两个方法,其中一个方法是实例方法,另一个方法是静态方法。为此,再次使用本章前面定义的Currency结构。

   struct Currency

   {

      public uint Dollars;

      public ushort Cents;

 

      public Currency(uint dollars, ushort cents)

      {

         this.Dollars = dollars;

         this.Cents = cents;

      }

      public override string ToString()

      {

         return string.Format("${0}.{1,2:00}", Dollars,Cents);

      }

      public static explicit operator Currency (float value)

      {

         checked

         {

            uint dollars =(uint)value;

            ushort cents =(ushort)((value-dollars)*100);

            return new Currency(dollars,cents);

          }

      }

      public static implicit operator float (Currency value)

      {

         return value.Dollars + (value.Cents/100.0f);

      }

public static implicit operator Currency (uint value)

{

   return new Currency(value, 0);

}

      public static implicit operator uint (Currency value)

      {

         return value.Dollars;

      }

   }

Currency结构已经有了自己的ToString()重载方法。为了说明如何使用带有静态方法的委托,再增加一个静态方法,其签名与Currency的签名相同:

   struct Currency

   {

      public static string GetCurrencyUnit()

      {

         return "Dollar";

      }

下面就可以使用GetAString 实例,代码如下所示:

      private delegate string GetAString();

 

      static void Main(string[] args)

      {

         int x = 40;

         GetAString firstStringMethod = new GetAString(x.ToString);

         Console.WriteLine("String is " + firstStringMethod());

         Currency balance = new Currency(34, 50);

         firstStringMethod = new GetAString(balance.ToString);

         Console.WriteLine("String is " + firstStringMethod());

         firstStringMethod = new GetAString(Currency.GetCurrencyUnit);

         Console.WriteLine("String is " + firstStringMethod());

这段代码说明了如何通过委托来调用方法,然后重新给委托指定在类的不同实例上执行的不同方法,甚至可以指定静态方法,或者在类的不同类型的实例上执行的方法,只要每个方法的签名匹配于委托定义即可。

但是,我们还没有说明把一个委托传递给另一个方法的具体过程,也没有给出任何有用的结果。调用intCurrency对象的ToString()的方法要比使用委托直观得多!在真正领会到委托的用途前,需要用一个相当复杂的示例来说明委托的本质。下面就是两个委托的示例。第一个示例仅使用委托来调用两个不同的操作,说明了如何把委托传递给方法,如何使用委托数组,但这仍没有很好地说明没有委托,就不能完成很多工作。第二个示例就复杂得多了,它有一个类BubbleSorter,执行一个方法,按照升序排列一个对象数组,这个类没有委托是很难编写出来的。