委托与事件(一)委托

Callback functions are an important part of programming in Windows. If you have a background in C or C++ programming, you have seen callbacks used in many of the Windows APIs. Callback functions are really pointers to a method call. Also known as function pointers, they are a very powerful programming feature. .NET has implemented the concept of a function pointer in the form of delegates. What makes them special is that, unlike the C function pointer, the .NET delegate is type - safe. What this means is that a function pointer in C is nothing but a pointer to a memory location. You have no idea what that pointer is really pointing to. Things like parameters and return types are not known. As you see in this chapter, .NET has made delegates a type - safe operation. Later in the chapter, you see how .NET uses delegates as the means of implementing events.

                                                                  -------《C#高级编程》

回调函数在C/C++程序中使用广泛。回调函数实际上是指向函数的指针。但回调函数的缺点是,它直接指向内存块。因此,它不是类型安全的。.NET中的委托是回调函数的面向对象的实现。它是类型安全的。.NET中的事件也使用了委托。

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

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在语法上不允许使用这种直接的方法。如果要传递方法,就必须把方法的细节封装在一种新类型的对象中,即委托。委托只是一种特殊的对象类型,其特殊之处在于,我们以前定义的所有对象都包含数据,而委托包含的只是方法的细节。 

C#中声明委托

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

定义委托的语法如下:

delegate void VoidOperation(uint x);

在这个示例中,定义了一个委托VoidOperation,并指定该委托的每个实例都包含一个方法的细节,该方法带有一个uint参数,并返回void。在定义委托时,必须给出它所代表的方法的全部细节。

因为定义委托基本上是定义一个新类,所以可以在定义类的任何地方定义委托,既可以在另一个类的内部定义,也可以在任何类的外部定义,还可以在命名空间中把委托定义为顶层对象。根据定义的可见性,可以在委托定义上添加一般的访问修饰符:public private protected等:

public delegate string GetAString();

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

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());

}  

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

提示:

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

3 简单的委托示例

namespace DelegateTest
{
    
class MathsOperations
    {
        
public static double MultiplyByTwo(double value)
        {
            
return value * 2;
        }
        
public static double Square(double value)
        {
            
return value * value;
        }
    }
}

namespace DelegateTest
{
    
delegate double DoubleOp(double x);//定义委托
    
class Program
    {
        
static void DisplayNumber(DoubleOp action, double value)
        {
            
double result = action(value);
            Console.WriteLine(
               
"Value is {0}, result of operation is {1}", value, result);
        }

        
static void Main(string[] args)
        {
            DoubleOp[] operations 
=//委托的实例数组
            {
               
new DoubleOp(MathsOperations.MultiplyByTwo),//委托的实例可以表示任何类型的任何对象上的实例方法或静态方法
               
new DoubleOp(MathsOperations.Square)
            };
            
for (int i = 0; i < operations.Length; i++)
            {
                Console.WriteLine(
"Using operations[{0}]:", i);
                DisplayNumber(operations[i], 
2.0);
                DisplayNumber(operations[i], 
7.94);
                DisplayNumber(operations[i], 
1.414);
                Console.WriteLine();
            }
        }
    }
}

结果:

Using operations[0]:
Value is 2, result of operation is 4
Value is 7.94, result of operation is 15.88
Value is 1.414, result of operation is 2.828

Using operations[1]:
Value is 2, result of operation is 4
Value is 7.94, result of operation is 63.0436
Value is 1.414, result of operation is 1.999396

 

如果在这个例子中使用匿名方法,就可以删除第一个类MathOperationsMain方法应如下所示:

static void Main(string[] args)
        {
            
//使用匿名方法
            DoubleOp multByTwo = delegate(double val) {return val * 2;};
            DoubleOp square 
= delegate(double val) { return val * val; };
            DoubleOp [] operations 
= {multByTwo, square};
            
for (int i=0 ; i<operations.Length ; i++)
            {
                Console.WriteLine(
"Using operations[{0}]:", i);
                DisplayNumber(operations[i], 
2.0);
                DisplayNumber(operations[i], 
7.94);
                DisplayNumber(operations[i], 
1.414);
                Console.WriteLine();
            }
        }

运行这个版本,结果与前面的例子相同。其优点是删除了一个类MathsOperations

匿名方法的优点是减少了系统开销。方法仅在由委托使用时才定义。在为事件定义委托时,这是非常显然的。这有助于降低代码的复杂性,尤其是定义了好几个方法时,代码会显得比较简单。

在使用匿名方法时,必须遵循两个规则。在匿名方法中不能使用跳转语句跳到该匿名方法的外部,反之亦然:匿名方法外部的跳转语句不能跳到该匿名方法的内部。

在匿名方法内部不能访问不安全的代码。另外,也不能访问在匿名方法外部使用的refout参数。但可以使用在匿名方法外部定义的其他变量。

4 多播委托

前面使用的每个委托都只包含一个方法调用。调用委托的次数与调用方法的次数相同。如果要调用多个方法,就需要多次显式调用这个委托。委托也可以包含多个方法。这种委托称为多播委托。如果调用多播委托,就可以按顺序连续调用多个方法。为此,委托的签名就必须返回void(否则,返回值应送到何处?)。实际上,如果编译器发现某个委托返回void,就会自动假定这是一个多播委托。

在前面的示例中,要存储对两个方法的引用,所以实例化了一个委托数组。而这里只是在一个多播委托中添加两个操作。多播委托可以识别运算符++=

namespace DelegateTest
{
    
class MathsOperations2
    {
        
//使用多播委托.委托的签名就必须返回void
        public static void  MultiplyByTwo(double value)
        {
            
double result= value * 2;
            Console.WriteLine(
               
"Value is {0}, result of operation is {1}", value, result);
        }
        
public static void Square(double value)
        {
            
double result = value * value;
            Console.WriteLine(
               
"Value is {0}, result of operation is {1}", value, result);
        }
    }
}

namespace DelegateTest
{
    
delegate void DoubleOp(double x);//定义委托,委托的签名就必须返回void
    class Program2
    {
        
static void DisplayNumber(DoubleOp action, double value)
        {
            action(value);
        }

        
static void Main(string[] args)
        {            
            
//使用多播委托
            DoubleOp operations = new DoubleOp(MathsOperations2.MultiplyByTwo);
            operations 
+= new DoubleOp(MathsOperations2.Square);
            DisplayNumber(operations, 
2.0);
            DisplayNumber(operations, 
7.94);
            DisplayNumber(operations, 
1.414);
            Console.WriteLine();
        }
    }
}

 

Value is 2, result of operation is 4
Value is 2, result of operation is 4
Value is 7.94, result of operation is 15.88
Value is 7.94, result of operation is 63.0436
Value is 1.414, result of operation is 2.828
Value is 1.414, result of operation is 1.999396

 

posted on 2009-05-24 15:43  HEYUTAO  阅读(169)  评论(0编辑  收藏  举报